From fb87e94c5ad716e63f32896793c971a82495d6e3 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Fri, 29 May 2026 22:00:15 +0900 Subject: [PATCH] refactor: centralize EJClaw runtime env names (#192) --- runners/agent-runner/src/index.ts | 17 ++++---- runners/agent-runner/src/ipc-mcp-stdio.ts | 19 +++++---- runners/agent-runner/src/ipc-paths.ts | 5 ++- runners/agent-runner/src/mcp-config.ts | 15 ++++--- runners/agent-runner/test/mcp-config.test.ts | 15 ++++--- runners/codex-runner/src/index.ts | 7 +-- runners/shared/src/ejclaw-env.ts | 16 +++++++ runners/shared/src/index.ts | 1 + src/agent-runner-environment.test.ts | 15 ++++--- src/agent-runner-environment.ts | 45 +++++++++++--------- src/agent-runner.ts | 15 ++++--- 11 files changed, 100 insertions(+), 70 deletions(-) create mode 100644 runners/shared/src/ejclaw-env.ts diff --git a/runners/agent-runner/src/index.ts b/runners/agent-runner/src/index.ts index 26f1898..5bee5bc 100644 --- a/runners/agent-runner/src/index.ts +++ b/runners/agent-runner/src/index.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import path from 'path'; import { query } from '@anthropic-ai/claude-agent-sdk'; import { + EJCLAW_ENV, IPC_CLOSE_SENTINEL, IPC_INPUT_SUBDIR, IPC_POLL_MS, @@ -78,12 +79,12 @@ interface RunnerInput { } // Paths configurable via env vars. -const GROUP_DIR = process.env.EJCLAW_GROUP_DIR || '/workspace/group'; -const IPC_DIR = process.env.EJCLAW_IPC_DIR || '/workspace/ipc'; -const HOST_IPC_DIR = process.env.EJCLAW_HOST_IPC_DIR || IPC_DIR; +const GROUP_DIR = process.env[EJCLAW_ENV.groupDir] || '/workspace/group'; +const IPC_DIR = process.env[EJCLAW_ENV.ipcDir] || '/workspace/ipc'; +const HOST_IPC_DIR = process.env[EJCLAW_ENV.hostIpcDir] || IPC_DIR; // Optional: override cwd (agent works in this directory instead of GROUP_DIR) -const WORK_DIR = process.env.EJCLAW_WORK_DIR || ''; -const GROUP_FOLDER = process.env.EJCLAW_GROUP_FOLDER || ''; +const WORK_DIR = process.env[EJCLAW_ENV.workDir] || ''; +const GROUP_FOLDER = process.env[EJCLAW_ENV.groupFolder] || ''; const IPC_INPUT_DIR = path.join(IPC_DIR, IPC_INPUT_SUBDIR); const IPC_INPUT_CLOSE_SENTINEL = path.join(IPC_INPUT_DIR, IPC_CLOSE_SENTINEL); @@ -333,10 +334,10 @@ async function runQuery( chatJid: runnerInput.chatJid, groupFolder: runnerInput.groupFolder, isMain: runnerInput.isMain, - agentType: process.env.EJCLAW_AGENT_TYPE || 'claude-code', + agentType: process.env[EJCLAW_ENV.agentType] || 'claude-code', roomRole: runnerInput.roomRoleContext?.role || '', - ipcDir: process.env.EJCLAW_IPC_DIR, - hostIpcDir: process.env.EJCLAW_HOST_IPC_DIR, + ipcDir: process.env[EJCLAW_ENV.ipcDir], + hostIpcDir: process.env[EJCLAW_ENV.hostIpcDir], }), }, hooks: { diff --git a/runners/agent-runner/src/ipc-mcp-stdio.ts b/runners/agent-runner/src/ipc-mcp-stdio.ts index ee23187..2aa62e0 100644 --- a/runners/agent-runner/src/ipc-mcp-stdio.ts +++ b/runners/agent-runner/src/ipc-mcp-stdio.ts @@ -10,6 +10,7 @@ import { z } from 'zod'; import fs from 'fs'; import path from 'path'; import { CronExpressionParser } from 'cron-parser'; +import { EJCLAW_ENV } from 'ejclaw-runners-shared'; import { buildCiWatchPrompt, DEFAULT_WATCH_CI_CONTEXT_MODE, @@ -34,15 +35,15 @@ const MESSAGES_DIR = path.join(HOST_IPC_DIR, 'messages'); const TASKS_DIR = path.join(HOST_IPC_DIR, 'tasks'); const HOST_EVIDENCE_RESPONSES_DIR = resolveHostEvidenceResponsesDir(HOST_IPC_DIR); -const REPO_ROOT = process.env.EJCLAW_WORK_DIR || process.cwd(); +const REPO_ROOT = process.env[EJCLAW_ENV.workDir] || process.cwd(); // Context from environment variables (set by the agent runner) -const chatJid = process.env.EJCLAW_CHAT_JID!; -const groupFolder = process.env.EJCLAW_GROUP_FOLDER!; -const isMain = process.env.EJCLAW_IS_MAIN === '1'; -const agentType = process.env.EJCLAW_AGENT_TYPE || 'claude-code'; -const roomRole = process.env.EJCLAW_ROOM_ROLE; -const runtimeTaskId = process.env.EJCLAW_RUNTIME_TASK_ID; +const chatJid = process.env[EJCLAW_ENV.chatJid]!; +const groupFolder = process.env[EJCLAW_ENV.groupFolder]!; +const isMain = process.env[EJCLAW_ENV.isMain] === '1'; +const agentType = process.env[EJCLAW_ENV.agentType] || 'claude-code'; +const roomRole = process.env[EJCLAW_ENV.roomRole]; +const runtimeTaskId = process.env[EJCLAW_ENV.runtimeTaskId]; const allowGenericScheduling = agentType !== 'codex'; function currentAgentType(): 'claude-code' | 'codex' { @@ -93,8 +94,8 @@ server.tool( chatJid, text: args.text, sender: args.sender || undefined, - senderRole: process.env.EJCLAW_ROOM_ROLE || undefined, - runId: process.env.EJCLAW_RUN_ID || undefined, + senderRole: process.env[EJCLAW_ENV.roomRole] || undefined, + runId: process.env[EJCLAW_ENV.runId] || undefined, groupFolder, }); diff --git a/runners/agent-runner/src/ipc-paths.ts b/runners/agent-runner/src/ipc-paths.ts index c9ee8e2..7f69c0f 100644 --- a/runners/agent-runner/src/ipc-paths.ts +++ b/runners/agent-runner/src/ipc-paths.ts @@ -1,4 +1,5 @@ import path from 'path'; +import { EJCLAW_ENV } from 'ejclaw-runners-shared'; export interface ResolvedIpcDirectories { ipcDir: string; @@ -13,8 +14,8 @@ export function isTaskScopedIpcDir(ipcDir: string): boolean { export function resolveIpcDirectories( env: NodeJS.ProcessEnv, ): ResolvedIpcDirectories { - const ipcDir = env.EJCLAW_IPC_DIR || '/workspace/ipc'; - const hostIpcDir = env.EJCLAW_HOST_IPC_DIR; + const ipcDir = env[EJCLAW_ENV.ipcDir] || '/workspace/ipc'; + const hostIpcDir = env[EJCLAW_ENV.hostIpcDir]; if (!hostIpcDir && isTaskScopedIpcDir(ipcDir)) { throw new Error( diff --git a/runners/agent-runner/src/mcp-config.ts b/runners/agent-runner/src/mcp-config.ts index d9a2715..60ac2ef 100644 --- a/runners/agent-runner/src/mcp-config.ts +++ b/runners/agent-runner/src/mcp-config.ts @@ -1,4 +1,5 @@ import type { McpStdioServerConfig } from '@anthropic-ai/claude-agent-sdk'; +import { EJCLAW_ENV } from 'ejclaw-runners-shared'; export interface EjclawMcpServerConfigInput { chatJid: string; @@ -19,16 +20,16 @@ export function buildEjclawMcpServerConfig( args: [mcpServerPath], alwaysLoad: true, env: { - EJCLAW_CHAT_JID: input.chatJid, - EJCLAW_GROUP_FOLDER: input.groupFolder, - EJCLAW_IS_MAIN: input.isMain ? '1' : '0', - EJCLAW_AGENT_TYPE: input.agentType, - EJCLAW_ROOM_ROLE: input.roomRole, + [EJCLAW_ENV.chatJid]: input.chatJid, + [EJCLAW_ENV.groupFolder]: input.groupFolder, + [EJCLAW_ENV.isMain]: input.isMain ? '1' : '0', + [EJCLAW_ENV.agentType]: input.agentType, + [EJCLAW_ENV.roomRole]: input.roomRole, ...(input.ipcDir && { - EJCLAW_IPC_DIR: input.ipcDir, + [EJCLAW_ENV.ipcDir]: input.ipcDir, }), ...(input.hostIpcDir && { - EJCLAW_HOST_IPC_DIR: input.hostIpcDir, + [EJCLAW_ENV.hostIpcDir]: input.hostIpcDir, }), }, }; diff --git a/runners/agent-runner/test/mcp-config.test.ts b/runners/agent-runner/test/mcp-config.test.ts index cc28ac4..042b56c 100644 --- a/runners/agent-runner/test/mcp-config.test.ts +++ b/runners/agent-runner/test/mcp-config.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { EJCLAW_ENV } from 'ejclaw-runners-shared'; import { buildEjclawMcpServerConfig } from '../src/mcp-config.js'; @@ -19,13 +20,13 @@ describe('ejclaw MCP config', () => { args: ['/runner/dist/ipc-mcp-stdio.js'], alwaysLoad: true, env: { - EJCLAW_CHAT_JID: 'dc:room', - EJCLAW_GROUP_FOLDER: 'ejclaw', - EJCLAW_IS_MAIN: '1', - EJCLAW_AGENT_TYPE: 'claude-code', - EJCLAW_ROOM_ROLE: 'reviewer', - EJCLAW_IPC_DIR: '/tmp/ipc/task', - EJCLAW_HOST_IPC_DIR: '/tmp/ipc/host', + [EJCLAW_ENV.chatJid]: 'dc:room', + [EJCLAW_ENV.groupFolder]: 'ejclaw', + [EJCLAW_ENV.isMain]: '1', + [EJCLAW_ENV.agentType]: 'claude-code', + [EJCLAW_ENV.roomRole]: 'reviewer', + [EJCLAW_ENV.ipcDir]: '/tmp/ipc/task', + [EJCLAW_ENV.hostIpcDir]: '/tmp/ipc/host', }, }); }); diff --git a/runners/codex-runner/src/index.ts b/runners/codex-runner/src/index.ts index 2ecbe0c..1c9d7f0 100644 --- a/runners/codex-runner/src/index.ts +++ b/runners/codex-runner/src/index.ts @@ -16,6 +16,7 @@ import fs from 'fs'; import path from 'path'; import { + EJCLAW_ENV, extractImageTagPaths, IPC_CLOSE_SENTINEL, IPC_INPUT_SUBDIR, @@ -71,9 +72,9 @@ interface RunnerOutput { // ── Constants ────────────────────────────────────────────────────── -const GROUP_DIR = process.env.EJCLAW_GROUP_DIR || '/workspace/group'; -const IPC_DIR = process.env.EJCLAW_IPC_DIR || '/workspace/ipc'; -const WORK_DIR = process.env.EJCLAW_WORK_DIR || ''; +const GROUP_DIR = process.env[EJCLAW_ENV.groupDir] || '/workspace/group'; +const IPC_DIR = process.env[EJCLAW_ENV.ipcDir] || '/workspace/ipc'; +const WORK_DIR = process.env[EJCLAW_ENV.workDir] || ''; const IPC_INPUT_DIR = path.join(IPC_DIR, IPC_INPUT_SUBDIR); const IPC_INPUT_CLOSE_SENTINEL = path.join(IPC_INPUT_DIR, IPC_CLOSE_SENTINEL); diff --git a/runners/shared/src/ejclaw-env.ts b/runners/shared/src/ejclaw-env.ts new file mode 100644 index 0000000..6e2e0b2 --- /dev/null +++ b/runners/shared/src/ejclaw-env.ts @@ -0,0 +1,16 @@ +export const EJCLAW_ENV = { + agentType: 'EJCLAW_AGENT_TYPE', + chatJid: 'EJCLAW_CHAT_JID', + globalDir: 'EJCLAW_GLOBAL_DIR', + groupDir: 'EJCLAW_GROUP_DIR', + groupFolder: 'EJCLAW_GROUP_FOLDER', + hostIpcDir: 'EJCLAW_HOST_IPC_DIR', + ipcDir: 'EJCLAW_IPC_DIR', + isMain: 'EJCLAW_IS_MAIN', + roomRole: 'EJCLAW_ROOM_ROLE', + runId: 'EJCLAW_RUN_ID', + runtimeTaskId: 'EJCLAW_RUNTIME_TASK_ID', + workDir: 'EJCLAW_WORK_DIR', +} as const; + +export type EjclawEnvName = (typeof EJCLAW_ENV)[keyof typeof EJCLAW_ENV]; diff --git a/runners/shared/src/index.ts b/runners/shared/src/index.ts index 118c607..937138f 100644 --- a/runners/shared/src/index.ts +++ b/runners/shared/src/index.ts @@ -2,6 +2,7 @@ export { prependRoomRoleHeader, type RoomRoleContext, } from './room-role-context.js'; +export { EJCLAW_ENV, type EjclawEnvName } from './ejclaw-env.js'; export { extractMarkdownImageAttachments, extractMediaAttachments, diff --git a/src/agent-runner-environment.test.ts b/src/agent-runner-environment.test.ts index 6592b1b..1c6b8f8 100644 --- a/src/agent-runner-environment.test.ts +++ b/src/agent-runner-environment.test.ts @@ -3,6 +3,7 @@ import os from 'os'; import path from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { EJCLAW_ENV } from 'ejclaw-runners-shared'; const { mockReadEnvFile, mockGetActiveCodexAuthPath } = vi.hoisted(() => ({ mockReadEnvFile: vi.fn<() => Record>(), @@ -455,7 +456,7 @@ describe('prepareGroupEnvironment Codex MCP room role env', () => { roomRole: 'owner', }); - expect(readCodexConfigToml()).toContain('EJCLAW_ROOM_ROLE = "owner"'); + expect(readCodexConfigToml()).toContain(`${EJCLAW_ENV.roomRole} = "owner"`); }); it('omits room role from non-paired Codex MCP config env', () => { @@ -464,7 +465,7 @@ describe('prepareGroupEnvironment Codex MCP room role env', () => { prepareGroupEnvironment(group, true, 'dc:test'); - expect(readCodexConfigToml()).not.toContain('EJCLAW_ROOM_ROLE'); + expect(readCodexConfigToml()).not.toContain(EJCLAW_ENV.roomRole); }); }); @@ -741,7 +742,7 @@ command = "node" args = ["old-ipc.js"] [mcp_servers.ejclaw.env] -EJCLAW_IPC_DIR = "/old/ipc" +${EJCLAW_ENV.ipcDir} = "/old/ipc" [mcp_servers.other] command = "node" @@ -780,10 +781,10 @@ args = ["other.js"] expect(toml).toContain('model = "gpt-5.4"'); expect(toml).toContain('[mcp_servers.other]'); expect(toml).toContain('[mcp_servers.ejclaw]'); - expect(toml).toContain('EJCLAW_IPC_DIR = "/workspace/ipc"'); - expect(toml).toContain('EJCLAW_GROUP_FOLDER = "codex-test-group"'); - expect(toml).toContain('EJCLAW_ROOM_ROLE = "reviewer"'); - expect(toml).toContain('EJCLAW_WORK_DIR = "/workspace/project"'); + expect(toml).toContain(`${EJCLAW_ENV.ipcDir} = "/workspace/ipc"`); + expect(toml).toContain(`${EJCLAW_ENV.groupFolder} = "codex-test-group"`); + expect(toml).toContain(`${EJCLAW_ENV.roomRole} = "reviewer"`); + expect(toml).toContain(`${EJCLAW_ENV.workDir} = "/workspace/project"`); expect(toml).not.toContain('old-ipc.js'); expect(toml).not.toContain('"/old/ipc"'); expect(toml.match(/\[mcp_servers\.ejclaw\]/g)).toHaveLength(1); diff --git a/src/agent-runner-environment.ts b/src/agent-runner-environment.ts index 12b309a..e857acb 100644 --- a/src/agent-runner-environment.ts +++ b/src/agent-runner-environment.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import { EJCLAW_ENV } from 'ejclaw-runners-shared'; import { GROUPS_DIR, @@ -210,20 +211,22 @@ function upsertEjclawMcpServerSection(args: { ? fs.readFileSync(args.sessionConfigPath, 'utf-8') : ''; toml = stripEjclawMcpServerSections(toml); + const tomlEnvLine = (name: string, value: string): string => + `${name} = ${JSON.stringify(value)}`; const mcpSection = ` [mcp_servers.ejclaw] command = "node" args = [${JSON.stringify(args.mcpServerPath)}] [mcp_servers.ejclaw.env] -EJCLAW_IPC_DIR = ${JSON.stringify(args.ipcDir)} -EJCLAW_HOST_IPC_DIR = ${JSON.stringify(args.hostIpcDir)} -EJCLAW_CHAT_JID = ${JSON.stringify(args.chatJid)} -EJCLAW_GROUP_FOLDER = ${JSON.stringify(args.groupFolder)} -EJCLAW_IS_MAIN = ${JSON.stringify(args.isMain ? '1' : '0')} -EJCLAW_AGENT_TYPE = ${JSON.stringify(args.agentType)} -${args.roomRole ? `EJCLAW_ROOM_ROLE = ${JSON.stringify(args.roomRole)}\n` : ''} -${args.workDir ? `EJCLAW_WORK_DIR = ${JSON.stringify(args.workDir)}\n` : ''} +${tomlEnvLine(EJCLAW_ENV.ipcDir, args.ipcDir)} +${tomlEnvLine(EJCLAW_ENV.hostIpcDir, args.hostIpcDir)} +${tomlEnvLine(EJCLAW_ENV.chatJid, args.chatJid)} +${tomlEnvLine(EJCLAW_ENV.groupFolder, args.groupFolder)} +${tomlEnvLine(EJCLAW_ENV.isMain, args.isMain ? '1' : '0')} +${tomlEnvLine(EJCLAW_ENV.agentType, args.agentType)} +${args.roomRole ? `${tomlEnvLine(EJCLAW_ENV.roomRole, args.roomRole)}\n` : ''} +${args.workDir ? `${tomlEnvLine(EJCLAW_ENV.workDir, args.workDir)}\n` : ''} `; fs.writeFileSync(args.sessionConfigPath, toml.trimEnd() + '\n' + mcpSection); } @@ -263,18 +266,18 @@ function buildBaseRunnerEnv(args: { : currentPath, TZ: TIMEZONE, HOME: os.homedir(), - EJCLAW_GROUP_DIR: args.groupDir, - EJCLAW_IPC_DIR: args.groupIpcDir, - EJCLAW_HOST_IPC_DIR: args.hostIpcDir, - EJCLAW_GLOBAL_DIR: args.globalDir, - ...(args.group.workDir ? { EJCLAW_WORK_DIR: args.group.workDir } : {}), - EJCLAW_CHAT_JID: args.chatJid, - EJCLAW_GROUP_FOLDER: args.group.folder, - EJCLAW_IS_MAIN: args.isMain ? '1' : '0', - EJCLAW_AGENT_TYPE: args.agentType, + [EJCLAW_ENV.groupDir]: args.groupDir, + [EJCLAW_ENV.ipcDir]: args.groupIpcDir, + [EJCLAW_ENV.hostIpcDir]: args.hostIpcDir, + [EJCLAW_ENV.globalDir]: args.globalDir, + ...(args.group.workDir ? { [EJCLAW_ENV.workDir]: args.group.workDir } : {}), + [EJCLAW_ENV.chatJid]: args.chatJid, + [EJCLAW_ENV.groupFolder]: args.group.folder, + [EJCLAW_ENV.isMain]: args.isMain ? '1' : '0', + [EJCLAW_ENV.agentType]: args.agentType, CLAUDE_CONFIG_DIR: args.groupSessionsDir, ...(args.runtimeTaskId - ? { EJCLAW_RUNTIME_TASK_ID: args.runtimeTaskId } + ? { [EJCLAW_ENV.runtimeTaskId]: args.runtimeTaskId } : {}), }; } @@ -486,15 +489,15 @@ function prepareCodexSessionEnvironment(args: { upsertEjclawMcpServerSection({ sessionConfigPath, mcpServerPath, - ipcDir: args.env.EJCLAW_IPC_DIR, - hostIpcDir: args.env.EJCLAW_HOST_IPC_DIR, + ipcDir: args.env[EJCLAW_ENV.ipcDir], + hostIpcDir: args.env[EJCLAW_ENV.hostIpcDir], chatJid: args.chatJid, groupFolder: args.group.folder, isMain: args.isMain, agentType: 'codex', roomRole: args.roomRole, workDir: - args.env.EJCLAW_WORK_DIR || args.group.workDir || args.projectRoot, + args.env[EJCLAW_ENV.workDir] || args.group.workDir || args.projectRoot, }); } diff --git a/src/agent-runner.ts b/src/agent-runner.ts index e52d811..fae5827 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -2,7 +2,10 @@ import { ChildProcess, spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { isUnsafeHostPairedModeEnabled } from 'ejclaw-runners-shared'; +import { + EJCLAW_ENV, + isUnsafeHostPairedModeEnabled, +} from 'ejclaw-runners-shared'; /** * Agent Process Runner for EJClaw @@ -122,9 +125,9 @@ export async function runAgentProcess( agentType: group.agentType || 'claude-code', memoryBriefing: input.memoryBriefing, role: input.roomRoleContext.role, - ipcDir: env.EJCLAW_IPC_DIR, - hostIpcDir: env.EJCLAW_HOST_IPC_DIR, - workDir: envOverrides.EJCLAW_WORK_DIR || env.EJCLAW_WORK_DIR, + ipcDir: env[EJCLAW_ENV.ipcDir], + hostIpcDir: env[EJCLAW_ENV.hostIpcDir], + workDir: envOverrides[EJCLAW_ENV.workDir] || env[EJCLAW_ENV.workDir], skillOverrides, }); if ((group.agentType || 'claude-code') === 'codex') { @@ -135,7 +138,7 @@ export async function runAgentProcess( } } if (input.runId) { - env.EJCLAW_RUN_ID = input.runId; + env[EJCLAW_ENV.runId] = input.runId; } const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-'); @@ -179,7 +182,7 @@ export async function runAgentProcess( env, }); - onProcess(proc, processName, env.EJCLAW_IPC_DIR); + onProcess(proc, processName, env[EJCLAW_ENV.ipcDir]); const runnerInput: AgentInput = { ...input,