diff --git a/src/create-tools.ts b/src/create-tools.ts index 880e0a427..5ac5a7e2f 100644 --- a/src/create-tools.ts +++ b/src/create-tools.ts @@ -9,7 +9,7 @@ import { createAvailableCategories } from "./plugin/available-categories" import { createSkillContext } from "./plugin/skill-context" import { createToolRegistry } from "./plugin/tool-registry" -export type CreateToolsResult = { +type CreateToolsResult = { filteredTools: ToolsRecord mergedSkills: LoadedSkill[] availableSkills: AvailableSkill[] diff --git a/src/features/builtin-commands/commands.ts b/src/features/builtin-commands/commands.ts index fe581498d..8daa361df 100644 --- a/src/features/builtin-commands/commands.ts +++ b/src/features/builtin-commands/commands.ts @@ -9,7 +9,7 @@ import { START_WORK_TEMPLATE } from "./templates/start-work" import { HANDOFF_TEMPLATE } from "./templates/handoff" import { REMOVE_AI_SLOPS_TEMPLATE } from "./templates/remove-ai-slops" -export interface LoadBuiltinCommandsOptions { +interface LoadBuiltinCommandsOptions { useRegisteredAgents?: boolean } diff --git a/src/openclaw/config.ts b/src/openclaw/config.ts index 846f6d912..c501f5c8d 100644 --- a/src/openclaw/config.ts +++ b/src/openclaw/config.ts @@ -3,6 +3,7 @@ import type { OpenClawGateway, OpenClawReplyListenerConfig, } from "./types" +export { validateGatewayUrl } from "./gateway-url-validation" const DEFAULT_REPLY_POLL_INTERVAL_MS = 3000 const MIN_REPLY_POLL_INTERVAL_MS = 500 @@ -97,22 +98,3 @@ export function resolveGateway( return { gatewayName: mapping.gateway, gateway, instruction: mapping.instruction } } - -export function validateGatewayUrl(url: string): boolean { - try { - const parsed = new URL(url) - if (parsed.protocol === "https:") return true - if ( - parsed.protocol === "http:" && - (parsed.hostname === "localhost" || - parsed.hostname === "127.0.0.1" || - parsed.hostname === "::1" || - parsed.hostname === "[::1]") - ) { - return true - } - return false - } catch { - return false - } -} diff --git a/src/openclaw/dispatcher.ts b/src/openclaw/dispatcher.ts index 173819c93..5971f371d 100644 --- a/src/openclaw/dispatcher.ts +++ b/src/openclaw/dispatcher.ts @@ -1,30 +1,11 @@ import { spawn } from "bun" +import { validateGatewayUrl } from "./gateway-url-validation" import type { OpenClawGateway, WakeResult } from "./types" const DEFAULT_HTTP_TIMEOUT_MS = 10_000 const DEFAULT_COMMAND_TIMEOUT_MS = 5_000 const MIN_COMMAND_TIMEOUT_MS = 100 const MAX_COMMAND_TIMEOUT_MS = 300_000 -const SHELL_METACHAR_RE = /[|&;><`$()]/ - -export function validateGatewayUrl(url: string): boolean { - try { - const parsed = new URL(url) - if (parsed.protocol === "https:") return true - if ( - parsed.protocol === "http:" && - (parsed.hostname === "localhost" || - parsed.hostname === "127.0.0.1" || - parsed.hostname === "::1" || - parsed.hostname === "[::1]") - ) { - return true - } - return false - } catch { - return false - } -} export function interpolateInstruction( template: string, diff --git a/src/openclaw/gateway-url-validation.test.ts b/src/openclaw/gateway-url-validation.test.ts new file mode 100644 index 000000000..aadf60e8a --- /dev/null +++ b/src/openclaw/gateway-url-validation.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test" +import { validateGatewayUrl } from "./gateway-url-validation" + +describe("validateGatewayUrl", () => { + test("allows https and local http while rejecting remote or invalid urls", () => { + // given representative gateway urls + const httpsRemote = "https://example.com" + const httpRemote = "http://example.com" + const httpLocalhost = "http://localhost:3000" + const httpLoopback = "http://127.0.0.1:3000" + const httpIpv6Loopback = "http://[::1]:3000" + const invalidUrl = "not-a-url" + + // when validating each url + const results = { + httpsRemote: validateGatewayUrl(httpsRemote), + httpRemote: validateGatewayUrl(httpRemote), + httpLocalhost: validateGatewayUrl(httpLocalhost), + httpLoopback: validateGatewayUrl(httpLoopback), + httpIpv6Loopback: validateGatewayUrl(httpIpv6Loopback), + invalidUrl: validateGatewayUrl(invalidUrl), + } + + // then only https and localhost loopback urls are allowed + expect(results).toEqual({ + httpsRemote: true, + httpRemote: false, + httpLocalhost: true, + httpLoopback: true, + httpIpv6Loopback: true, + invalidUrl: false, + }) + }) +}) diff --git a/src/openclaw/gateway-url-validation.ts b/src/openclaw/gateway-url-validation.ts new file mode 100644 index 000000000..4d37be9df --- /dev/null +++ b/src/openclaw/gateway-url-validation.ts @@ -0,0 +1,18 @@ +export function validateGatewayUrl(url: string): boolean { + try { + const parsed = new URL(url) + if (parsed.protocol === "https:") return true + if ( + parsed.protocol === "http:" && + (parsed.hostname === "localhost" || + parsed.hostname === "127.0.0.1" || + parsed.hostname === "::1" || + parsed.hostname === "[::1]") + ) { + return true + } + return false + } catch { + return false + } +} diff --git a/src/shared/agent-display-names.test.ts b/src/shared/agent-display-names.test.ts index 57cdd3c27..2c3d732cd 100644 --- a/src/shared/agent-display-names.test.ts +++ b/src/shared/agent-display-names.test.ts @@ -217,6 +217,10 @@ describe("normalizeAgentForPrompt", () => { it("removes zero-width characters before returning canonical names", () => { expect(normalizeAgentForPrompt("Sisyphus\u200B - Ultraworker")).toBe("Sisyphus - Ultraworker") }) + + it("converts legacy parenthesized names to canonical display names", () => { + expect(normalizeAgentForPrompt("Atlas (Plan Executor)")).toBe("Atlas - Plan Executor") + }) }) describe("normalizeAgentForPromptKey", () => { @@ -224,6 +228,10 @@ describe("normalizeAgentForPromptKey", () => { expect(normalizeAgentForPromptKey("Sisyphus (Ultraworker)")).toBe("sisyphus") }) + it("strips UI ordering prefixes before returning config keys", () => { + expect(normalizeAgentForPromptKey(getAgentListDisplayName("atlas"))).toBe("atlas") + }) + it("preserves custom agents", () => { expect(normalizeAgentForPromptKey("MyCustomAgent")).toBe("MyCustomAgent") }) @@ -259,7 +267,7 @@ describe("AGENT_DISPLAY_NAMES", () => { const httpHeaderUnsafe = /[()]/ // when checking each display name - for (const [key, displayName] of Object.entries(AGENT_DISPLAY_NAMES)) { + for (const [, displayName] of Object.entries(AGENT_DISPLAY_NAMES)) { // then none should contain parentheses expect(httpHeaderUnsafe.test(displayName)).toBe(false) } diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 6747d445f..324fac785 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -94,18 +94,23 @@ const LEGACY_DISPLAY_NAMES: Record = { "athena-junior (council)": "athena-junior", } -/** - * Resolve an agent name (display name or config key) to its lowercase config key. - * "Atlas - Plan Executor" -> "atlas", "Atlas (Plan Executor)" -> "atlas", "atlas" -> "atlas" - */ -export function getAgentConfigKey(agentName: string): string { +function resolveKnownAgentConfigKey(agentName: string): string | undefined { const lower = stripAgentListSortPrefix(agentName).trim().toLowerCase() const reversed = REVERSE_DISPLAY_NAMES[lower] if (reversed !== undefined) return reversed const legacy = LEGACY_DISPLAY_NAMES[lower] if (legacy !== undefined) return legacy if (AGENT_DISPLAY_NAMES[lower] !== undefined) return lower - return lower + return undefined +} + +/** + * Resolve an agent name (display name or config key) to its lowercase config key. + * "Atlas - Plan Executor" -> "atlas", "Atlas (Plan Executor)" -> "atlas", "atlas" -> "atlas" + */ +export function getAgentConfigKey(agentName: string): string { + const lower = stripAgentListSortPrefix(agentName).trim().toLowerCase() + return resolveKnownAgentConfigKey(agentName) ?? lower } /** @@ -124,17 +129,9 @@ export function normalizeAgentForPrompt(agentName: string | undefined): string | return undefined } - const lower = trimmed.toLowerCase() - const reversed = REVERSE_DISPLAY_NAMES[lower] - if (reversed !== undefined) { - return AGENT_DISPLAY_NAMES[reversed] ?? trimmed - } - const legacy = LEGACY_DISPLAY_NAMES[lower] - if (legacy !== undefined) { - return AGENT_DISPLAY_NAMES[legacy] ?? trimmed - } - if (AGENT_DISPLAY_NAMES[lower] !== undefined) { - return AGENT_DISPLAY_NAMES[lower] + const configKey = resolveKnownAgentConfigKey(trimmed) + if (configKey !== undefined) { + return AGENT_DISPLAY_NAMES[configKey] ?? trimmed } return trimmed @@ -150,18 +147,5 @@ export function normalizeAgentForPromptKey(agentName: string | undefined): strin return undefined } - const lower = trimmed.toLowerCase() - const reversed = REVERSE_DISPLAY_NAMES[lower] - if (reversed !== undefined) { - return reversed - } - const legacy = LEGACY_DISPLAY_NAMES[lower] - if (legacy !== undefined) { - return legacy - } - if (AGENT_DISPLAY_NAMES[lower] !== undefined) { - return lower - } - - return trimmed + return resolveKnownAgentConfigKey(trimmed) ?? trimmed }