From 239da8b02a73d174391cf073a26c7cf6cf161b52 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 16 Mar 2026 21:09:08 +0900 Subject: [PATCH] Revert "Merge pull request #2607 from code-yeongyu/feat/openclaw-integration" This reverts commit 8213534e873e1bc5fab5c5afa1480b4fa680ae74, reversing changes made to 84fb1113f1df15d4799e927848e26052a2d772fa. --- src/config/schema/hooks.ts | 1 - src/config/schema/oh-my-opencode-config.ts | 2 - src/config/schema/openclaw.ts | 51 ---- src/hooks/openclaw-sender/index.ts | 70 ----- src/openclaw/__tests__/client.test.ts | 41 --- src/openclaw/__tests__/config.test.ts | 40 --- src/openclaw/__tests__/dispatcher.test.ts | 78 ----- src/openclaw/client.ts | 256 ----------------- src/openclaw/dispatcher.ts | 317 --------------------- src/openclaw/index.ts | 10 - src/openclaw/types.ts | 134 --------- src/plugin/event.ts | 1 - src/plugin/hooks/create-session-hooks.ts | 8 - src/plugin/tool-execute-before.ts | 1 - 14 files changed, 1010 deletions(-) delete mode 100644 src/config/schema/openclaw.ts delete mode 100644 src/hooks/openclaw-sender/index.ts delete mode 100644 src/openclaw/__tests__/client.test.ts delete mode 100644 src/openclaw/__tests__/config.test.ts delete mode 100644 src/openclaw/__tests__/dispatcher.test.ts delete mode 100644 src/openclaw/client.ts delete mode 100644 src/openclaw/dispatcher.ts delete mode 100644 src/openclaw/index.ts delete mode 100644 src/openclaw/types.ts diff --git a/src/config/schema/hooks.ts b/src/config/schema/hooks.ts index e7a83a4d9..4acc37584 100644 --- a/src/config/schema/hooks.ts +++ b/src/config/schema/hooks.ts @@ -51,7 +51,6 @@ export const HookNameSchema = z.enum([ "anthropic-effort", "hashline-read-enhancer", "read-image-resizer", - "openclaw-sender", ]) export type HookName = z.infer diff --git a/src/config/schema/oh-my-opencode-config.ts b/src/config/schema/oh-my-opencode-config.ts index ea7e479c5..9f4d70c99 100644 --- a/src/config/schema/oh-my-opencode-config.ts +++ b/src/config/schema/oh-my-opencode-config.ts @@ -12,7 +12,6 @@ import { BuiltinCommandNameSchema } from "./commands" import { ExperimentalConfigSchema } from "./experimental" import { GitMasterConfigSchema } from "./git-master" import { NotificationConfigSchema } from "./notification" -import { OpenClawConfigSchema } from "./openclaw" import { RalphLoopConfigSchema } from "./ralph-loop" import { RuntimeFallbackConfigSchema } from "./runtime-fallback" import { SkillsConfigSchema } from "./skills" @@ -56,7 +55,6 @@ export const OhMyOpenCodeConfigSchema = z.object({ runtime_fallback: z.union([z.boolean(), RuntimeFallbackConfigSchema]).optional(), background_task: BackgroundTaskConfigSchema.optional(), notification: NotificationConfigSchema.optional(), - openclaw: OpenClawConfigSchema.optional(), babysitting: BabysittingConfigSchema.optional(), git_master: GitMasterConfigSchema.optional(), browser_automation_engine: BrowserAutomationConfigSchema.optional(), diff --git a/src/config/schema/openclaw.ts b/src/config/schema/openclaw.ts deleted file mode 100644 index aaa015126..000000000 --- a/src/config/schema/openclaw.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { z } from "zod"; - -export const OpenClawHookEventSchema = z.enum([ - "session-start", - "session-end", - "session-idle", - "ask-user-question", - "stop", -]); - -export const OpenClawHttpGatewayConfigSchema = z.object({ - type: z.literal("http").optional(), - url: z.string(), // Allow looser URL validation as it might contain placeholders - headers: z.record(z.string(), z.string()).optional(), - method: z.enum(["POST", "PUT"]).optional(), - timeout: z.number().optional(), -}); - -export const OpenClawCommandGatewayConfigSchema = z.object({ - type: z.literal("command"), - command: z.string(), - timeout: z.number().optional(), -}); - -export const OpenClawGatewayConfigSchema = z.union([ - OpenClawHttpGatewayConfigSchema, - OpenClawCommandGatewayConfigSchema, -]); - -export const OpenClawHookMappingSchema = z.object({ - gateway: z.string(), - instruction: z.string(), - enabled: z.boolean(), -}); - -export const OpenClawConfigSchema = z.object({ - enabled: z.boolean(), - gateways: z.record(z.string(), OpenClawGatewayConfigSchema), - hooks: z - .object({ - "session-start": OpenClawHookMappingSchema.optional(), - "session-end": OpenClawHookMappingSchema.optional(), - "session-idle": OpenClawHookMappingSchema.optional(), - "ask-user-question": OpenClawHookMappingSchema.optional(), - stop: OpenClawHookMappingSchema.optional(), - }) - .strict() - .optional(), -}); - -export type OpenClawConfig = z.infer; diff --git a/src/hooks/openclaw-sender/index.ts b/src/hooks/openclaw-sender/index.ts deleted file mode 100644 index 6d87f749c..000000000 --- a/src/hooks/openclaw-sender/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { wakeOpenClaw } from "../../openclaw/client"; -import type { OpenClawConfig, OpenClawContext } from "../../openclaw/types"; -import { getMainSessionID } from "../../features/claude-code-session-state"; -import type { PluginContext } from "../../plugin/types"; - -export function createOpenClawSenderHook( - ctx: PluginContext, - config: OpenClawConfig -) { - return { - event: async (input: { - event: { type: string; properties?: Record }; - }) => { - const { type, properties } = input.event; - const info = properties?.info as Record | undefined; - const context: OpenClawContext = { - sessionId: - (properties?.sessionID as string) || - (info?.id as string) || - getMainSessionID(), - projectPath: ctx.directory, - }; - - if (type === "session.created") { - await wakeOpenClaw("session-start", context, config); - } else if (type === "session.idle") { - await wakeOpenClaw("session-idle", context, config); - } else if (type === "session.deleted") { - await wakeOpenClaw("session-end", context, config); - } - }, - - "tool.execute.before": async ( - input: { tool: string; sessionID: string }, - output: { args: Record } - ) => { - const toolName = input.tool.toLowerCase(); - const context: OpenClawContext = { - sessionId: input.sessionID, - projectPath: ctx.directory, - }; - - if ( - toolName === "ask_user_question" || - toolName === "askuserquestion" || - toolName === "question" - ) { - const question = - typeof output.args.question === "string" - ? output.args.question - : undefined; - await wakeOpenClaw( - "ask-user-question", - { - ...context, - question, - }, - config - ); - } else if (toolName === "skill") { - const rawName = - typeof output.args.name === "string" ? output.args.name : undefined; - const command = rawName?.replace(/^\//, "").toLowerCase(); - if (command === "stop-continuation") { - await wakeOpenClaw("stop", context, config); - } - } - }, - }; -} diff --git a/src/openclaw/__tests__/client.test.ts b/src/openclaw/__tests__/client.test.ts deleted file mode 100644 index b260c44e8..000000000 --- a/src/openclaw/__tests__/client.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, it, expect } from "bun:test"; -import { resolveGateway } from "../client"; -import { type OpenClawConfig } from "../types"; - -describe("OpenClaw Client", () => { - describe("resolveGateway", () => { - const config: OpenClawConfig = { - enabled: true, - gateways: { - foo: { type: "command", command: "echo foo" }, - bar: { type: "http", url: "https://example.com" }, - }, - hooks: { - "session-start": { - gateway: "foo", - instruction: "start", - enabled: true, - }, - "session-end": { gateway: "bar", instruction: "end", enabled: true }, - stop: { gateway: "foo", instruction: "stop", enabled: false }, - }, - }; - - it("resolves valid mapping", () => { - const result = resolveGateway(config, "session-start"); - expect(result).not.toBeNull(); - expect(result?.gatewayName).toBe("foo"); - expect(result?.instruction).toBe("start"); - }); - - it("returns null for disabled hook", () => { - const result = resolveGateway(config, "stop"); - expect(result).toBeNull(); - }); - - it("returns null for unmapped event", () => { - const result = resolveGateway(config, "ask-user-question"); - expect(result).toBeNull(); - }); - }); -}); diff --git a/src/openclaw/__tests__/config.test.ts b/src/openclaw/__tests__/config.test.ts deleted file mode 100644 index 3d85a9f7c..000000000 --- a/src/openclaw/__tests__/config.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, it, expect } from "bun:test"; -import { OpenClawConfigSchema } from "../../config/schema/openclaw"; - -describe("OpenClaw Config Schema", () => { - it("validates correct config", () => { - const raw = { - enabled: true, - gateways: { - foo: { type: "command", command: "echo foo" }, - bar: { type: "http", url: "https://example.com" }, - }, - hooks: { - "session-start": { - gateway: "foo", - instruction: "start", - enabled: true, - }, - }, - }; - const parsed = OpenClawConfigSchema.safeParse(raw); - if (!parsed.success) console.log(parsed.error); - expect(parsed.success).toBe(true); - }); - - it("fails on invalid event", () => { - const raw = { - enabled: true, - gateways: {}, - hooks: { - "invalid-event": { - gateway: "foo", - instruction: "start", - enabled: true, - }, - }, - }; - const parsed = OpenClawConfigSchema.safeParse(raw); - expect(parsed.success).toBe(false); - }); -}); diff --git a/src/openclaw/__tests__/dispatcher.test.ts b/src/openclaw/__tests__/dispatcher.test.ts deleted file mode 100644 index d670092a1..000000000 --- a/src/openclaw/__tests__/dispatcher.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, it, expect } from "bun:test"; -import { - interpolateInstruction, - resolveCommandTimeoutMs, - shellEscapeArg, - validateGatewayUrl, - wakeCommandGateway, -} from "../dispatcher"; -import { type OpenClawCommandGatewayConfig } from "../types"; - -describe("OpenClaw Dispatcher", () => { - describe("validateGatewayUrl", () => { - it("accepts valid https URLs", () => { - expect(validateGatewayUrl("https://example.com")).toBe(true); - }); - - it("rejects http URLs (remote)", () => { - expect(validateGatewayUrl("http://example.com")).toBe(false); - }); - - it("accepts http URLs for localhost", () => { - expect(validateGatewayUrl("http://localhost:3000")).toBe(true); - expect(validateGatewayUrl("http://127.0.0.1:8080")).toBe(true); - }); - }); - - describe("interpolateInstruction", () => { - it("interpolates variables correctly", () => { - const result = interpolateInstruction("Hello {{name}}!", { name: "World" }); - expect(result).toBe("Hello World!"); - }); - - it("handles missing variables", () => { - const result = interpolateInstruction("Hello {{name}}!", {}); - expect(result).toBe("Hello !"); - }); - }); - - describe("shellEscapeArg", () => { - it("escapes simple string", () => { - expect(shellEscapeArg("foo")).toBe("'foo'"); - }); - - it("escapes string with single quotes", () => { - expect(shellEscapeArg("it's")).toBe("'it'\\''s'"); - }); - }); - - describe("resolveCommandTimeoutMs", () => { - it("uses default timeout", () => { - expect(resolveCommandTimeoutMs(undefined, undefined)).toBe(5000); - }); - - it("uses provided timeout", () => { - expect(resolveCommandTimeoutMs(1000, undefined)).toBe(1000); - }); - - it("clamps timeout", () => { - expect(resolveCommandTimeoutMs(10, undefined)).toBe(100); - expect(resolveCommandTimeoutMs(1000000, undefined)).toBe(300000); - }); - }); - - describe("wakeCommandGateway", () => { - it("rejects if disabled via env", async () => { - const oldEnv = process.env.OMX_OPENCLAW_COMMAND; - process.env.OMX_OPENCLAW_COMMAND = "0"; - const config: OpenClawCommandGatewayConfig = { - type: "command", - command: "echo hi", - }; - const result = await wakeCommandGateway("test", config, {}); - expect(result.success).toBe(false); - expect(result.error).toContain("disabled"); - process.env.OMX_OPENCLAW_COMMAND = oldEnv; - }); - }); -}); diff --git a/src/openclaw/client.ts b/src/openclaw/client.ts deleted file mode 100644 index 202f905f8..000000000 --- a/src/openclaw/client.ts +++ /dev/null @@ -1,256 +0,0 @@ -/** - * OpenClaw Integration - Client - * - * Wakes OpenClaw gateways on hook events. Non-blocking, fire-and-forget. - * - * Usage: - * wakeOpenClaw("session-start", { sessionId, projectPath: directory }, config); - * - * Activation requires OMX_OPENCLAW=1 env var and config in pluginConfig.openclaw. - */ - -import { - type OpenClawConfig, - type OpenClawContext, - type OpenClawHookEvent, - type OpenClawResult, - type OpenClawGatewayConfig, - type OpenClawHttpGatewayConfig, - type OpenClawCommandGatewayConfig, - type OpenClawPayload, -} from "./types"; -import { - interpolateInstruction, - isCommandGateway, - wakeCommandGateway, - wakeGateway, -} from "./dispatcher"; -import { execSync } from "child_process"; -import { basename } from "path"; - -/** Whether debug logging is enabled */ -const DEBUG = process.env.OMX_OPENCLAW_DEBUG === "1"; - -// Helper for tmux session -function getCurrentTmuxSession(): string | undefined { - if (!process.env.TMUX) return undefined; - try { - // tmux display-message -p '#S' - const session = execSync("tmux display-message -p '#S'", { - encoding: "utf-8", - }).trim(); - return session || undefined; - } catch { - return undefined; - } -} - -// Helper for tmux capture -function captureTmuxPane(paneId: string, lines: number): string | undefined { - try { - // tmux capture-pane -p -t {paneId} -S -{lines} - const output = execSync( - `tmux capture-pane -p -t "${paneId}" -S -${lines}`, - { encoding: "utf-8" } - ); - return output || undefined; - } catch { - return undefined; - } -} - -/** - * Build a whitelisted context object from the input context. - * Only known fields are included to prevent accidental data leakage. - */ -function buildWhitelistedContext(context: OpenClawContext): OpenClawContext { - const result: OpenClawContext = {}; - if (context.sessionId !== undefined) result.sessionId = context.sessionId; - if (context.projectPath !== undefined) - result.projectPath = context.projectPath; - if (context.tmuxSession !== undefined) - result.tmuxSession = context.tmuxSession; - if (context.prompt !== undefined) result.prompt = context.prompt; - if (context.contextSummary !== undefined) - result.contextSummary = context.contextSummary; - if (context.reason !== undefined) result.reason = context.reason; - if (context.question !== undefined) result.question = context.question; - if (context.tmuxTail !== undefined) result.tmuxTail = context.tmuxTail; - if (context.replyChannel !== undefined) - result.replyChannel = context.replyChannel; - if (context.replyTarget !== undefined) - result.replyTarget = context.replyTarget; - if (context.replyThread !== undefined) - result.replyThread = context.replyThread; - return result; -} - -/** - * Resolve gateway config for a specific hook event. - * Returns null if the event is not mapped or disabled. - * Returns the gateway name alongside config to avoid O(n) reverse lookup. - */ -export function resolveGateway( - config: OpenClawConfig, - event: OpenClawHookEvent -): { - gatewayName: string; - gateway: OpenClawGatewayConfig; - instruction: string; -} | null { - const mapping = config.hooks?.[event]; - if (!mapping || !mapping.enabled) { - return null; - } - const gateway = config.gateways?.[mapping.gateway]; - if (!gateway) { - return null; - } - // Validate based on gateway type - if (gateway.type === "command") { - if (!gateway.command) return null; - } else { - // HTTP gateway (default when type is absent or "http") - if (!("url" in gateway) || !gateway.url) return null; - } - return { - gatewayName: mapping.gateway, - gateway, - instruction: mapping.instruction, - }; -} - -/** - * Wake the OpenClaw gateway mapped to a hook event. - * - * This is the main entry point called from the notify hook. - * Non-blocking, swallows all errors. Returns null if OpenClaw - * is not configured or the event is not mapped. - * - * @param event - The hook event type - * @param context - Context data for template variable interpolation - * @param config - OpenClaw configuration - * @returns OpenClawResult or null if not configured/mapped - */ -export async function wakeOpenClaw( - event: OpenClawHookEvent, - context: OpenClawContext, - config?: OpenClawConfig -): Promise { - try { - // Activation gate: only active when OMX_OPENCLAW=1 - if (process.env.OMX_OPENCLAW !== "1") { - return null; - } - - if (!config || !config.enabled) return null; - - const resolved = resolveGateway(config, event); - if (!resolved) return null; - - const { gatewayName, gateway, instruction } = resolved; - const now = new Date().toISOString(); - - // Read originating channel context from env vars - const replyChannel = - context.replyChannel ?? process.env.OPENCLAW_REPLY_CHANNEL ?? undefined; - const replyTarget = - context.replyTarget ?? process.env.OPENCLAW_REPLY_TARGET ?? undefined; - const replyThread = - context.replyThread ?? process.env.OPENCLAW_REPLY_THREAD ?? undefined; - - // Merge reply context - const enrichedContext: OpenClawContext = { - ...context, - ...(replyChannel !== undefined && { replyChannel }), - ...(replyTarget !== undefined && { replyTarget }), - ...(replyThread !== undefined && { replyThread }), - }; - - // Auto-detect tmux session - const tmuxSession = - enrichedContext.tmuxSession ?? getCurrentTmuxSession() ?? undefined; - - // Auto-capture tmux pane content - let tmuxTail = enrichedContext.tmuxTail; - if ( - !tmuxTail && - (event === "stop" || event === "session-end") && - process.env.TMUX - ) { - const paneId = process.env.TMUX_PANE; - if (paneId) { - tmuxTail = captureTmuxPane(paneId, 15) ?? undefined; - } - } - - // Build template variables - const variables: Record = { - sessionId: enrichedContext.sessionId, - projectPath: enrichedContext.projectPath, - projectName: enrichedContext.projectPath - ? basename(enrichedContext.projectPath) - : undefined, - tmuxSession, - prompt: enrichedContext.prompt, - contextSummary: enrichedContext.contextSummary, - reason: enrichedContext.reason, - question: enrichedContext.question, - tmuxTail, - event, - timestamp: now, - replyChannel, - replyTarget, - replyThread, - }; - - // Interpolate instruction - const interpolatedInstruction = interpolateInstruction( - instruction, - variables - ); - variables.instruction = interpolatedInstruction; - - let result: OpenClawResult; - - if (isCommandGateway(gateway)) { - result = await wakeCommandGateway(gatewayName, gateway, variables); - } else { - const payload: OpenClawPayload = { - event, - instruction: interpolatedInstruction, - text: interpolatedInstruction, - timestamp: now, - sessionId: enrichedContext.sessionId, - projectPath: enrichedContext.projectPath, - projectName: enrichedContext.projectPath - ? basename(enrichedContext.projectPath) - : undefined, - tmuxSession, - tmuxTail, - ...(replyChannel !== undefined && { channel: replyChannel }), - ...(replyTarget !== undefined && { to: replyTarget }), - ...(replyThread !== undefined && { threadId: replyThread }), - context: buildWhitelistedContext(enrichedContext), - }; - result = await wakeGateway(gatewayName, gateway, payload); - } - - if (DEBUG) { - console.error( - `[openclaw] wake ${event} -> ${gatewayName}: ${ - result.success ? "ok" : result.error - }` - ); - } - return result; - } catch (error) { - if (DEBUG) { - console.error( - `[openclaw] wakeOpenClaw error:`, - error instanceof Error ? error.message : error - ); - } - return null; - } -} diff --git a/src/openclaw/dispatcher.ts b/src/openclaw/dispatcher.ts deleted file mode 100644 index e17bcd918..000000000 --- a/src/openclaw/dispatcher.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * OpenClaw Gateway Dispatcher - * - * Sends instruction payloads to OpenClaw gateways via HTTP or CLI command. - * All calls are non-blocking with timeouts. Failures are swallowed - * to avoid blocking hooks. - * - * SECURITY: Command gateway requires OMX_OPENCLAW_COMMAND=1 opt-in. - * Command timeout is configurable with safe bounds. - * Prefers execFile for simple commands; falls back to sh -c only for shell metacharacters. - */ - -import { - type OpenClawCommandGatewayConfig, - type OpenClawGatewayConfig, - type OpenClawHttpGatewayConfig, - type OpenClawPayload, - type OpenClawResult, -} from "./types"; -import { exec, execFile } from "child_process"; - -/** Default per-request timeout for HTTP gateways */ -const DEFAULT_HTTP_TIMEOUT_MS = 10_000; -/** Default command gateway timeout (backward-compatible default) */ -const DEFAULT_COMMAND_TIMEOUT_MS = 5_000; -/** - * Command timeout safety bounds. - * - Minimum 100ms: avoids immediate/near-zero timeout misconfiguration. - * - Maximum 300000ms (5 minutes): prevents runaway long-lived command processes. - */ -const MIN_COMMAND_TIMEOUT_MS = 100; -const MAX_COMMAND_TIMEOUT_MS = 300_000; - -/** Shell metacharacters that require sh -c instead of execFile */ -const SHELL_METACHAR_RE = /[|&;><`$()]/; - -/** - * Validate gateway URL. Must be HTTPS, except localhost/127.0.0.1/::1 - * which allows HTTP for local development. - */ -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 (err) { - process.stderr.write(`[openclaw-dispatcher] operation failed: ${err}\n`); - return false; - } -} - -/** - * Interpolate template variables in an instruction string. - * - * Supported variables (from hook context): - * - {{projectName}} - basename of project directory - * - {{projectPath}} - full project directory path - * - {{sessionId}} - session identifier - * - {{prompt}} - prompt text - * - {{contextSummary}} - context summary (session-end event) - * - {{question}} - question text (ask-user-question event) - * - {{timestamp}} - ISO timestamp - * - {{event}} - hook event name - * - {{instruction}} - interpolated instruction (for command gateway) - * - {{replyChannel}} - originating channel (from OPENCLAW_REPLY_CHANNEL env var) - * - {{replyTarget}} - reply target user/bot (from OPENCLAW_REPLY_TARGET env var) - * - {{replyThread}} - reply thread ID (from OPENCLAW_REPLY_THREAD env var) - * - * Unresolved variables are replaced with empty string. - */ -export function interpolateInstruction( - template: string, - variables: Record -): string { - return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => { - return variables[key] ?? ""; - }); -} - -/** - * Type guard: is this gateway config a command gateway? - */ -export function isCommandGateway( - config: OpenClawGatewayConfig -): config is OpenClawCommandGatewayConfig { - return config.type === "command"; -} - -/** - * Shell-escape a string for safe embedding in a shell command. - * Uses single-quote wrapping with internal quote escaping. - */ -export function shellEscapeArg(value: string): string { - return "'" + value.replace(/'/g, "'\\''") + "'"; -} - -/** - * Resolve command gateway timeout with precedence: - * gateway timeout > OMX_OPENCLAW_COMMAND_TIMEOUT_MS > default. - */ -export function resolveCommandTimeoutMs( - gatewayTimeout?: number, - envTimeoutRaw = process.env.OMX_OPENCLAW_COMMAND_TIMEOUT_MS -): number { - const parseFinite = (value: unknown): number | undefined => { - if (typeof value !== "number" || !Number.isFinite(value)) return undefined; - return value; - }; - const parseEnv = (value: string | undefined): number | undefined => { - if (!value) return undefined; - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : undefined; - }; - - const rawTimeout = - parseFinite(gatewayTimeout) ?? - parseEnv(envTimeoutRaw) ?? - DEFAULT_COMMAND_TIMEOUT_MS; - - return Math.min( - MAX_COMMAND_TIMEOUT_MS, - Math.max(MIN_COMMAND_TIMEOUT_MS, Math.trunc(rawTimeout)) - ); -} - -/** - * Wake an HTTP-type OpenClaw gateway with the given payload. - */ -export async function wakeGateway( - gatewayName: string, - gatewayConfig: OpenClawHttpGatewayConfig, - payload: OpenClawPayload -): Promise { - if (!validateGatewayUrl(gatewayConfig.url)) { - return { - gateway: gatewayName, - success: false, - error: "Invalid URL (HTTPS required)", - }; - } - - try { - const headers = { - "Content-Type": "application/json", - ...gatewayConfig.headers, - }; - const timeout = gatewayConfig.timeout ?? DEFAULT_HTTP_TIMEOUT_MS; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - const response = await fetch(gatewayConfig.url, { - method: gatewayConfig.method || "POST", - headers, - body: JSON.stringify(payload), - signal: controller.signal, - }); - clearTimeout(timeoutId); - - if (!response.ok) { - return { - gateway: gatewayName, - success: false, - error: `HTTP ${response.status}`, - statusCode: response.status, - }; - } - - return { gateway: gatewayName, success: true, statusCode: response.status }; - } catch (error) { - return { - gateway: gatewayName, - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }; - } -} - -/** - * Wake a command-type OpenClaw gateway by executing a shell command. - * - * SECURITY REQUIREMENTS: - * - Requires OMX_OPENCLAW_COMMAND=1 opt-in (separate gate from OMX_OPENCLAW) - * - Timeout is configurable via gateway.timeout or OMX_OPENCLAW_COMMAND_TIMEOUT_MS - * with safe clamping bounds and backward-compatible default 5000ms - * - Prefers execFile for simple commands (no metacharacters) - * - Falls back to sh -c only when metacharacters detected - * - detached: false to prevent orphan processes - * - SIGTERM cleanup handler kills child on parent SIGTERM, 1s grace then SIGKILL - * - * The command template supports {{variable}} placeholders. All variable - * values are shell-escaped before interpolation to prevent injection. - */ -export async function wakeCommandGateway( - gatewayName: string, - gatewayConfig: OpenClawCommandGatewayConfig, - variables: Record -): Promise { - // Separate command gateway opt-in gate - if (process.env.OMX_OPENCLAW_COMMAND !== "1") { - return { - gateway: gatewayName, - success: false, - error: "Command gateway disabled (set OMX_OPENCLAW_COMMAND=1 to enable)", - }; - } - - let child: any = null; - let sigtermHandler: (() => void) | null = null; - - try { - const timeout = resolveCommandTimeoutMs(gatewayConfig.timeout); - - // Interpolate variables with shell escaping - const interpolated = gatewayConfig.command.replace( - /\{\{(\w+)\}\}/g, - (match, key) => { - const value = variables[key]; - if (value === undefined) return match; - return shellEscapeArg(value); - } - ); - - // Detect whether the interpolated command contains shell metacharacters - const hasMetachars = SHELL_METACHAR_RE.test(interpolated); - - await new Promise((resolve, reject) => { - const cleanup = (signal: NodeJS.Signals) => { - if (child) { - child.kill(signal); - // 1s grace period then SIGKILL - setTimeout(() => { - try { - child?.kill("SIGKILL"); - } catch (err) { - process.stderr.write( - `[openclaw-dispatcher] operation failed: ${err}\n` - ); - } - }, 1000); - } - }; - - sigtermHandler = () => cleanup("SIGTERM"); - process.once("SIGTERM", sigtermHandler); - - const onExit = (code: number | null, signal: NodeJS.Signals | null) => { - if (sigtermHandler) { - process.removeListener("SIGTERM", sigtermHandler); - sigtermHandler = null; - } - - if (signal) { - reject(new Error(`Command killed by signal ${signal}`)); - } else if (code !== 0) { - reject(new Error(`Command exited with code ${code}`)); - } else { - resolve(); - } - }; - - const onError = (err: Error) => { - if (sigtermHandler) { - process.removeListener("SIGTERM", sigtermHandler); - sigtermHandler = null; - } - reject(err); - }; - - if (hasMetachars) { - // Fall back to sh -c for complex commands with metacharacters - child = exec(interpolated, { - timeout, - env: { ...process.env }, - }); - } else { - // Parse simple command: split on whitespace, use execFile - const parts = interpolated.split(/\s+/).filter(Boolean); - const cmd = parts[0]; - const args = parts.slice(1); - child = execFile(cmd, args, { - timeout, - env: { ...process.env }, - }); - } - - // Ensure detached is false (default, but explicit via options above) - if (child) { - child.on("exit", onExit); - child.on("error", onError); - } else { - reject(new Error("Failed to spawn process")); - } - }); - - return { gateway: gatewayName, success: true }; - } catch (error) { - // Ensure SIGTERM handler is cleaned up on error - if (sigtermHandler) { - process.removeListener("SIGTERM", sigtermHandler as () => void); - } - return { - gateway: gatewayName, - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }; - } -} diff --git a/src/openclaw/index.ts b/src/openclaw/index.ts deleted file mode 100644 index b872dfb21..000000000 --- a/src/openclaw/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { resolveGateway, wakeOpenClaw } from "./client"; -export { - interpolateInstruction, - isCommandGateway, - shellEscapeArg, - validateGatewayUrl, - wakeCommandGateway, - wakeGateway, -} from "./dispatcher"; -export * from "./types"; diff --git a/src/openclaw/types.ts b/src/openclaw/types.ts deleted file mode 100644 index b7e4747f2..000000000 --- a/src/openclaw/types.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * OpenClaw Gateway Integration Types - * - * Defines types for the OpenClaw gateway waker system. - * Each hook event can be mapped to a gateway with a pre-defined instruction. - */ - -/** Hook events that can trigger OpenClaw gateway calls */ -export type OpenClawHookEvent = - | "session-start" - | "session-end" - | "session-idle" - | "ask-user-question" - | "stop"; - -/** HTTP gateway configuration (default when type is absent or "http") */ -export interface OpenClawHttpGatewayConfig { - /** Gateway type discriminator (optional for backward compat) */ - type?: "http"; - /** Gateway endpoint URL (HTTPS required, HTTP allowed for localhost) */ - url: string; - /** Optional custom headers (e.g., Authorization) */ - headers?: Record; - /** HTTP method (default: POST) */ - method?: "POST" | "PUT"; - /** Per-request timeout in ms (default: 10000) */ - timeout?: number; -} - -/** CLI command gateway configuration */ -export interface OpenClawCommandGatewayConfig { - /** Gateway type discriminator */ - type: "command"; - /** Command template with {{variable}} placeholders. - * Variables are shell-escaped automatically before interpolation. */ - command: string; - /** - * Per-command timeout in ms. - * Precedence: gateway timeout > OMX_OPENCLAW_COMMAND_TIMEOUT_MS > default (5000ms). - * Runtime clamps to safe bounds. - */ - timeout?: number; -} - -/** Gateway configuration — HTTP or CLI command */ -export type OpenClawGatewayConfig = - | OpenClawHttpGatewayConfig - | OpenClawCommandGatewayConfig; - -/** Per-hook-event mapping to a gateway + instruction */ -export interface OpenClawHookMapping { - /** Name of the gateway (key in gateways object) */ - gateway: string; - /** Instruction template with {{variable}} placeholders */ - instruction: string; - /** Whether this hook-event mapping is active */ - enabled: boolean; -} - -/** Top-level config schema for notifications.openclaw key in .omx-config.json */ -export interface OpenClawConfig { - /** Global enable/disable */ - enabled: boolean; - /** Named gateway endpoints */ - gateways: Record; - /** Hook-event to gateway+instruction mappings */ - hooks?: Partial>; -} - -/** Payload sent to an OpenClaw gateway */ -export interface OpenClawPayload { - /** The hook event that triggered this call */ - event: OpenClawHookEvent; - /** Interpolated instruction text */ - instruction: string; - /** Alias of instruction — allows OpenClaw /hooks/wake to consume the payload directly */ - text: string; - /** ISO timestamp */ - timestamp: string; - /** Session identifier (if available) */ - sessionId?: string; - /** Project directory path */ - projectPath?: string; - /** Project basename */ - projectName?: string; - /** Tmux session name (if running inside tmux) */ - tmuxSession?: string; - /** Recent tmux pane output (for stop/session-end events) */ - tmuxTail?: string; - /** Originating channel for reply routing (if OPENCLAW_REPLY_CHANNEL is set) */ - channel?: string; - /** Reply target user/bot (if OPENCLAW_REPLY_TARGET is set) */ - to?: string; - /** Reply thread ID (if OPENCLAW_REPLY_THREAD is set) */ - threadId?: string; - /** Context data from the hook (whitelisted fields only) */ - context: OpenClawContext; -} - -/** - * Context data passed from the hook to OpenClaw for template interpolation. - * - * All fields are explicitly enumerated (no index signature) to prevent - * accidental leakage of sensitive data into gateway payloads. - */ -export interface OpenClawContext { - sessionId?: string; - projectPath?: string; - tmuxSession?: string; - prompt?: string; - contextSummary?: string; - reason?: string; - question?: string; - /** Recent tmux pane output (captured automatically for stop/session-end events) */ - tmuxTail?: string; - /** Originating channel for reply routing (from OPENCLAW_REPLY_CHANNEL env var) */ - replyChannel?: string; - /** Reply target user/bot (from OPENCLAW_REPLY_TARGET env var) */ - replyTarget?: string; - /** Reply thread ID for threaded conversations (from OPENCLAW_REPLY_THREAD env var) */ - replyThread?: string; -} - -/** Result of a gateway wake attempt */ -export interface OpenClawResult { - /** Gateway name */ - gateway: string; - /** Whether the call succeeded */ - success: boolean; - /** Error message if failed */ - error?: string; - /** HTTP status code if available */ - statusCode?: number; -} diff --git a/src/plugin/event.ts b/src/plugin/event.ts index 2b5a83058..d18ec3691 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -215,7 +215,6 @@ export function createEventHandler(args: { await Promise.resolve(hooks.compactionTodoPreserver?.event?.(input)); await Promise.resolve(hooks.writeExistingFileGuard?.event?.(input)); await Promise.resolve(hooks.atlasHook?.handler?.(input)); - await Promise.resolve(hooks.openclawSender?.event?.(input)); await Promise.resolve(hooks.autoSlashCommand?.event?.(input)); }; diff --git a/src/plugin/hooks/create-session-hooks.ts b/src/plugin/hooks/create-session-hooks.ts index 60ab77db4..daa5e4ff5 100644 --- a/src/plugin/hooks/create-session-hooks.ts +++ b/src/plugin/hooks/create-session-hooks.ts @@ -26,7 +26,6 @@ import { createPreemptiveCompactionHook, createRuntimeFallbackHook, } from "../../hooks" -import { createOpenClawSenderHook } from "../../hooks/openclaw-sender" import { createAnthropicEffortHook } from "../../hooks/anthropic-effort" import { detectExternalNotificationPlugin, @@ -61,7 +60,6 @@ export type SessionHooks = { taskResumeInfo: ReturnType | null anthropicEffort: ReturnType | null runtimeFallback: ReturnType | null - openclawSender: ReturnType | null } export function createSessionHooks(args: { @@ -263,11 +261,6 @@ export function createSessionHooks(args: { pluginConfig, })) : null - - const openclawSender = isHookEnabled("openclaw-sender") && pluginConfig.openclaw?.enabled - ? safeHook("openclaw-sender", () => createOpenClawSenderHook(ctx, pluginConfig.openclaw!)) - : null - return { contextWindowMonitor, preemptiveCompaction, @@ -292,6 +285,5 @@ export function createSessionHooks(args: { taskResumeInfo, anthropicEffort, runtimeFallback, - openclawSender, } } diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 173d7e82d..000b121dd 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -33,7 +33,6 @@ export function createToolExecuteBeforeHandler(args: { await hooks.prometheusMdOnly?.["tool.execute.before"]?.(input, output) await hooks.sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output) await hooks.atlasHook?.["tool.execute.before"]?.(input, output) - await hooks.openclawSender?.["tool.execute.before"]?.(input, output) const normalizedToolName = input.tool.toLowerCase() if (