From 17244e2c848f589d19692fb9e0ab7c6f017d6b7c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 16 Mar 2026 21:09:08 +0900 Subject: [PATCH 1/2] Revert "Merge pull request #2609 from code-yeongyu/fix/rename-omx-to-omo-env" This reverts commit 4759dfb65416220418ca0753eaf043ed82db60f4, reversing changes made to 8213534e873e1bc5fab5c5afa1480b4fa680ae74. --- src/openclaw/__tests__/client.test.ts | 61 +---------------------- src/openclaw/__tests__/dispatcher.test.ts | 6 +-- src/openclaw/client.ts | 8 +-- src/openclaw/dispatcher.ts | 14 +++--- src/openclaw/types.ts | 2 +- 5 files changed, 17 insertions(+), 74 deletions(-) diff --git a/src/openclaw/__tests__/client.test.ts b/src/openclaw/__tests__/client.test.ts index d173c6ee0..b260c44e8 100644 --- a/src/openclaw/__tests__/client.test.ts +++ b/src/openclaw/__tests__/client.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { resolveGateway, wakeOpenClaw } from "../client"; +import { describe, it, expect } from "bun:test"; +import { resolveGateway } from "../client"; import { type OpenClawConfig } from "../types"; describe("OpenClaw Client", () => { @@ -38,61 +38,4 @@ describe("OpenClaw Client", () => { expect(result).toBeNull(); }); }); - - describe("wakeOpenClaw env gate", () => { - let oldEnv: string | undefined; - - beforeEach(() => { - oldEnv = process.env.OMO_OPENCLAW; - }); - - afterEach(() => { - if (oldEnv === undefined) { - delete process.env.OMO_OPENCLAW; - } else { - process.env.OMO_OPENCLAW = oldEnv; - } - }); - - it("returns null when OMO_OPENCLAW is not set", async () => { - delete process.env.OMO_OPENCLAW; - const config: OpenClawConfig = { - enabled: true, - gateways: { gw: { type: "command", command: "echo test" } }, - hooks: { - "session-start": { gateway: "gw", instruction: "hi", enabled: true }, - }, - }; - const result = await wakeOpenClaw("session-start", { projectPath: "/tmp" }, config); - expect(result).toBeNull(); - }); - - it("returns null when OMO_OPENCLAW is not '1'", async () => { - process.env.OMO_OPENCLAW = "0"; - const config: OpenClawConfig = { - enabled: true, - gateways: { gw: { type: "command", command: "echo test" } }, - hooks: { - "session-start": { gateway: "gw", instruction: "hi", enabled: true }, - }, - }; - const result = await wakeOpenClaw("session-start", { projectPath: "/tmp" }, config); - expect(result).toBeNull(); - }); - - it("does not use OMX_OPENCLAW (old env var)", async () => { - delete process.env.OMO_OPENCLAW; - process.env.OMX_OPENCLAW = "1"; - const config: OpenClawConfig = { - enabled: true, - gateways: { gw: { type: "command", command: "echo test" } }, - hooks: { - "session-start": { gateway: "gw", instruction: "hi", enabled: true }, - }, - }; - const result = await wakeOpenClaw("session-start", { projectPath: "/tmp" }, config); - expect(result).toBeNull(); - delete process.env.OMX_OPENCLAW; - }); - }); }); diff --git a/src/openclaw/__tests__/dispatcher.test.ts b/src/openclaw/__tests__/dispatcher.test.ts index 5bdc2b1ff..d670092a1 100644 --- a/src/openclaw/__tests__/dispatcher.test.ts +++ b/src/openclaw/__tests__/dispatcher.test.ts @@ -63,8 +63,8 @@ describe("OpenClaw Dispatcher", () => { describe("wakeCommandGateway", () => { it("rejects if disabled via env", async () => { - const oldEnv = process.env.OMO_OPENCLAW_COMMAND; - process.env.OMO_OPENCLAW_COMMAND = "0"; + const oldEnv = process.env.OMX_OPENCLAW_COMMAND; + process.env.OMX_OPENCLAW_COMMAND = "0"; const config: OpenClawCommandGatewayConfig = { type: "command", command: "echo hi", @@ -72,7 +72,7 @@ describe("OpenClaw Dispatcher", () => { const result = await wakeCommandGateway("test", config, {}); expect(result.success).toBe(false); expect(result.error).toContain("disabled"); - process.env.OMO_OPENCLAW_COMMAND = oldEnv; + process.env.OMX_OPENCLAW_COMMAND = oldEnv; }); }); }); diff --git a/src/openclaw/client.ts b/src/openclaw/client.ts index 61eba228d..202f905f8 100644 --- a/src/openclaw/client.ts +++ b/src/openclaw/client.ts @@ -6,7 +6,7 @@ * Usage: * wakeOpenClaw("session-start", { sessionId, projectPath: directory }, config); * - * Activation requires OMO_OPENCLAW=1 env var and config in pluginConfig.openclaw. + * Activation requires OMX_OPENCLAW=1 env var and config in pluginConfig.openclaw. */ import { @@ -29,7 +29,7 @@ import { execSync } from "child_process"; import { basename } from "path"; /** Whether debug logging is enabled */ -const DEBUG = process.env.OMO_OPENCLAW_DEBUG === "1"; +const DEBUG = process.env.OMX_OPENCLAW_DEBUG === "1"; // Helper for tmux session function getCurrentTmuxSession(): string | undefined { @@ -138,8 +138,8 @@ export async function wakeOpenClaw( config?: OpenClawConfig ): Promise { try { - // Activation gate: only active when OMO_OPENCLAW=1 - if (process.env.OMO_OPENCLAW !== "1") { + // Activation gate: only active when OMX_OPENCLAW=1 + if (process.env.OMX_OPENCLAW !== "1") { return null; } diff --git a/src/openclaw/dispatcher.ts b/src/openclaw/dispatcher.ts index 9ec4e290b..e17bcd918 100644 --- a/src/openclaw/dispatcher.ts +++ b/src/openclaw/dispatcher.ts @@ -5,7 +5,7 @@ * All calls are non-blocking with timeouts. Failures are swallowed * to avoid blocking hooks. * - * SECURITY: Command gateway requires OMO_OPENCLAW_COMMAND=1 opt-in. + * 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. */ @@ -105,11 +105,11 @@ export function shellEscapeArg(value: string): string { /** * Resolve command gateway timeout with precedence: - * gateway timeout > OMO_OPENCLAW_COMMAND_TIMEOUT_MS > default. + * gateway timeout > OMX_OPENCLAW_COMMAND_TIMEOUT_MS > default. */ export function resolveCommandTimeoutMs( gatewayTimeout?: number, - envTimeoutRaw = process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS + envTimeoutRaw = process.env.OMX_OPENCLAW_COMMAND_TIMEOUT_MS ): number { const parseFinite = (value: unknown): number | undefined => { if (typeof value !== "number" || !Number.isFinite(value)) return undefined; @@ -189,8 +189,8 @@ export async function wakeGateway( * Wake a command-type OpenClaw gateway by executing a shell command. * * SECURITY REQUIREMENTS: - * - Requires OMO_OPENCLAW_COMMAND=1 opt-in (separate gate from OMO_OPENCLAW) - * - Timeout is configurable via gateway.timeout or OMO_OPENCLAW_COMMAND_TIMEOUT_MS + * - 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 @@ -206,11 +206,11 @@ export async function wakeCommandGateway( variables: Record ): Promise { // Separate command gateway opt-in gate - if (process.env.OMO_OPENCLAW_COMMAND !== "1") { + if (process.env.OMX_OPENCLAW_COMMAND !== "1") { return { gateway: gatewayName, success: false, - error: "Command gateway disabled (set OMO_OPENCLAW_COMMAND=1 to enable)", + error: "Command gateway disabled (set OMX_OPENCLAW_COMMAND=1 to enable)", }; } diff --git a/src/openclaw/types.ts b/src/openclaw/types.ts index 194826d5a..b7e4747f2 100644 --- a/src/openclaw/types.ts +++ b/src/openclaw/types.ts @@ -36,7 +36,7 @@ export interface OpenClawCommandGatewayConfig { command: string; /** * Per-command timeout in ms. - * Precedence: gateway timeout > OMO_OPENCLAW_COMMAND_TIMEOUT_MS > default (5000ms). + * Precedence: gateway timeout > OMX_OPENCLAW_COMMAND_TIMEOUT_MS > default (5000ms). * Runtime clamps to safe bounds. */ timeout?: number; From 239da8b02a73d174391cf073a26c7cf6cf161b52 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 16 Mar 2026 21:09:08 +0900 Subject: [PATCH 2/2] 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 (