diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0f971ce9c..d545c386c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,13 +5,13 @@ }, "metadata": { "description": "Codex plugins to use in Claude Code for delegation and code review.", - "version": "1.0.45" + "version": "1.0.46" }, "plugins": [ { "name": "codex", "description": "Use Codex from Claude Code to review code or delegate tasks.", - "version": "1.0.45", + "version": "1.0.46", "author": { "name": "OpenAI" }, diff --git a/README.md b/README.md index 51f4902b2..fc3c08751 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,24 @@ A turn that goes silent is interrupted rather than left to hang. Four budgets de While several quick tools are in flight, the most patient one sets the window, since the watchdog is asking whether anything at all is happening on the turn. When one fires, the run fails with `failureClass: "stalled"`. +### MCP Servers + +Threads this plugin starts inherit whatever MCP servers your `~/.codex/config.toml` configures; the plugin does not add or configure any of its own. + +Those threads are non-interactive by construction — they run with `approvalPolicy: "never"` and there is nobody at the keyboard to answer a prompt — so **the plugin approves MCP tool calls on its own**. That is a real consequence worth knowing: an MCP server you have configured can be called, unattended, by any reviewer or delegated task this plugin runs, and an MCP tool is not confined by the thread's sandbox the way a shell command is. + +To keep a particular server out of these threads, name it in `CODEX_DISABLED_MCP_SERVERS`: + +| Variable | Default | What it does | +| --- | --- | --- | +| `CODEX_DISABLED_MCP_SERVERS` | *(unset)* | Comma-separated MCP server names to disable on threads this plugin starts. Your interactive `codex` sessions are unaffected. | + +```bash +export CODEX_DISABLED_MCP_SERVERS=codegraph,some-other-server +``` + +Names must match `[A-Za-z0-9_-]+` — the plugin splices each one into a Codex config path, and a name needing quotes would make Codex read it as a new server definition and fail the thread outright. Anything else is skipped with a warning on stderr, leaving that server enabled. + ### Moving The Work Over To Codex Delegated tasks and any [stop gate](#enabling-review-gate) run can also be directly resumed inside Codex by running `codex resume` either with the specific session ID you received from running `/codex:result` or `/codex:status` or by selecting it from the list. diff --git a/package-lock.json b/package-lock.json index 2d9530d52..8c392efb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openai/codex-plugin-cc", - "version": "1.0.45", + "version": "1.0.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openai/codex-plugin-cc", - "version": "1.0.45", + "version": "1.0.46", "license": "Apache-2.0", "devDependencies": { "@types/node": "^25.5.0", diff --git a/package.json b/package.json index 288f29824..378d2f75e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openai/codex-plugin-cc", - "version": "1.0.45", + "version": "1.0.46", "private": true, "type": "module", "description": "Use Codex from Claude Code to review code or delegate tasks.", diff --git a/plugins/codex/.claude-plugin/plugin.json b/plugins/codex/.claude-plugin/plugin.json index b95841a4a..946dcb9ba 100644 --- a/plugins/codex/.claude-plugin/plugin.json +++ b/plugins/codex/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex", - "version": "1.0.45", + "version": "1.0.46", "description": "Use Codex from Claude Code to review code or delegate tasks.", "author": { "name": "OpenAI" diff --git a/plugins/codex/scripts/lib/app-server.mjs b/plugins/codex/scripts/lib/app-server.mjs index 0dd9c6af5..2441bee2b 100644 --- a/plugins/codex/scripts/lib/app-server.mjs +++ b/plugins/codex/scripts/lib/app-server.mjs @@ -173,6 +173,24 @@ class AppServerClientBase { } handleServerRequest(message) { + if (message.method === "mcpServer/elicitation/request") { + // These threads use approvalPolicy "never" and have no interactive user to answer an + // approval gate; -32601 is interpreted by app-server as a user denial instead. + // Only the one shape we recognize — a form asking to run an MCP tool — is consented to. + // Anything else, including an unfamiliar approval kind or a url-mode flow, is declined: + // empty content is a meaningful answer to that form and to nothing else. + const isToolCallApproval = + message.params?.mode === "form" && + message.params?._meta?.codex_approval_kind === "mcp_tool_call"; + this.sendMessage({ + id: message.id, + result: isToolCallApproval + ? { action: "accept", content: {}, _meta: null } + : { action: "decline", content: null, _meta: null } + }); + return; + } + this.sendMessage({ id: message.id, error: buildJsonRpcError(-32601, `Unsupported server request: ${message.method}`) diff --git a/plugins/codex/scripts/lib/codex.mjs b/plugins/codex/scripts/lib/codex.mjs index 8e5675bc9..59f8dd469 100644 --- a/plugins/codex/scripts/lib/codex.mjs +++ b/plugins/codex/scripts/lib/codex.mjs @@ -93,11 +93,15 @@ function buildThreadParams(cwd, options = {}) { serviceName: SERVICE_NAME, ephemeral: options.ephemeral ?? true }; + const config = { ...(options.config ?? {}) }; + for (const name of resolveDisabledMcpServers()) { + config[`mcp_servers.${name}.enabled`] = false; + } if (options.writableRoots?.length > 0) { - params.config = { - ...(options.config ?? {}), - "sandbox_workspace_write.writable_roots": options.writableRoots - }; + config["sandbox_workspace_write.writable_roots"] = options.writableRoots; + } + if (Object.keys(config).length > 0) { + params.config = config; } return params; } @@ -111,11 +115,15 @@ function buildResumeParams(threadId, cwd, options = {}) { approvalPolicy: options.approvalPolicy ?? "never", sandbox: options.sandbox ?? "read-only" }; + const config = { ...(options.config ?? {}) }; + for (const name of resolveDisabledMcpServers()) { + config[`mcp_servers.${name}.enabled`] = false; + } if (options.writableRoots?.length > 0) { - params.config = { - ...(options.config ?? {}), - "sandbox_workspace_write.writable_roots": options.writableRoots - }; + config["sandbox_workspace_write.writable_roots"] = options.writableRoots; + } + if (Object.keys(config).length > 0) { + params.config = config; } return params; } @@ -194,6 +202,27 @@ function resolveToolMaxInFlightMs(options = {}) { return DEFAULT_TOOL_MAX_INFLIGHT_MS; } +function resolveDisabledMcpServers() { + const seen = new Set(); + const disabled = []; + for (const entry of (process.env.CODEX_DISABLED_MCP_SERVERS ?? "").split(",")) { + const name = entry.trim(); + if (!name || seen.has(name)) { + continue; + } + seen.add(name); + // Only a bare TOML key can be spliced into a dotted config path. Quoting the name instead + // makes Codex read the segment as a new server table with no transport, which fails + // thread/start for the whole run rather than just leaving that server enabled. + if (!/^[A-Za-z0-9_-]+$/.test(name)) { + process.stderr.write(`Skipping disabled MCP server "${name}": not a bare TOML key.\n`); + continue; + } + disabled.push(name); + } + return disabled; +} + function shorten(text, limit = 72) { const normalized = String(text ?? "").trim().replace(/\s+/g, " "); if (!normalized) { diff --git a/tests/fake-codex-fixture.mjs b/tests/fake-codex-fixture.mjs index 95ba709ac..578844bc2 100644 --- a/tests/fake-codex-fixture.mjs +++ b/tests/fake-codex-fixture.mjs @@ -16,6 +16,7 @@ const readline = require("node:readline"); const STATE_PATH = ${JSON.stringify(statePath)}; const BEHAVIOR = ${JSON.stringify(behavior)}; const interruptibleTurns = new Map(); + const pendingServerRequests = new Map(); if (BEHAVIOR === "close-stalls") { const keepAlive = setInterval(() => {}, 1000); process.on("SIGTERM", () => {}); @@ -212,6 +213,83 @@ function send(message) { process.stdout.write(JSON.stringify(message) + "\\n"); } +function requestServerReply(id, method, params, onReply) { + pendingServerRequests.set(id, onReply); + send({ id, method, params }); +} + +function emitMcpApprovalCompletion(state, threadId, turnId, reply) { + state.lastElicitationReply = reply; + saveState(state); + if (reply.error) { + send({ method: "turn/started", params: { threadId, turn: buildTurn(turnId) } }); + send({ method: "error", params: { threadId, turnId, error: { message: "user rejected MCP tool call" } } }); + send({ method: "turn/completed", params: { threadId, turn: buildTurn(turnId, "failed") } }); + return; + } + + const toolOutput = "MCP tool output: codegraph_status completed."; + send({ method: "turn/started", params: { threadId, turn: buildTurn(turnId) } }); + send({ + method: "item/started", + params: { + threadId, + turnId, + item: { + type: "mcpToolCall", + id: "mcp_" + turnId, + server: "codegraph", + tool: "codegraph_status", + status: "inProgress" + } + } + }); + send({ + method: "item/completed", + params: { + threadId, + turnId, + item: { + type: "mcpToolCall", + id: "mcp_" + turnId, + server: "codegraph", + tool: "codegraph_status", + status: "completed", + result: toolOutput + } + } + }); + send({ + method: "item/completed", + params: { + threadId, + turnId, + item: { type: "agentMessage", id: "msg_" + turnId, text: toolOutput, phase: "final_answer" } + } + }); + send({ method: "turn/completed", params: { threadId, turn: buildTurn(turnId, "completed") } }); +} + +function emitMcpDeclineCompletion(state, threadId, turnId, reply) { + state.lastElicitationReply = reply; + saveState(state); + if (reply.error) { + send({ method: "turn/started", params: { threadId, turn: buildTurn(turnId) } }); + send({ method: "error", params: { threadId, turnId, error: { message: "user rejected MCP tool call" } } }); + send({ method: "turn/completed", params: { threadId, turn: buildTurn(turnId, "failed") } }); + return; + } + + emitTurnCompleted(threadId, turnId, { + completed: { + type: "agentMessage", + id: "msg_" + turnId, + text: "MCP elicitation declined.", + phase: "final_answer" + } + }); +} + function nextThread(state, cwd, ephemeral) { const thread = { id: "thr_" + state.nextThreadId++, @@ -400,6 +478,12 @@ rl.on("line", (line) => { const message = JSON.parse(line); const state = loadState(); + const pendingReply = pendingServerRequests.get(message.id); + if (pendingReply) { + pendingServerRequests.delete(message.id); + pendingReply(message); + return; + } try { switch (message.method) { @@ -721,6 +805,124 @@ rl.on("line", (line) => { break; } + if (BEHAVIOR === "mcp-elicitation-approval") { + requestServerReply( + "elicitation_" + turnId, + "mcpServer/elicitation/request", + { + threadId: thread.id, + turnId, + serverName: "codegraph", + mode: "form", + _meta: { + codex_approval_kind: "mcp_tool_call", + persist: ["session", "always"], + tool_description: "Index health check (files / nodes / edges). Skip unless debugging.", + tool_params: {}, + tool_params_display: [] + }, + message: "Allow the codegraph MCP server to run tool \\"codegraph_status\\"?", + requestedSchema: { type: "object", properties: {} } + }, + (reply) => emitMcpApprovalCompletion(state, thread.id, turnId, reply) + ); + break; + } + + if (BEHAVIOR === "mcp-elicitation-decline") { + requestServerReply( + "elicitation_" + turnId, + "mcpServer/elicitation/request", + { + threadId: thread.id, + turnId, + serverName: "codegraph", + mode: "form", + _meta: {}, + message: "Please provide the requested codegraph filter.", + requestedSchema: { type: "object", properties: { filter: { type: "string" } } } + }, + (reply) => emitMcpDeclineCompletion(state, thread.id, turnId, reply) + ); + break; + } + + if (BEHAVIOR === "mcp-elicitation-unknown-kind") { + requestServerReply( + "elicitation_" + turnId, + "mcpServer/elicitation/request", + { + threadId: thread.id, + turnId, + serverName: "codegraph", + mode: "form", + _meta: { codex_approval_kind: "some_future_kind" }, + message: "Approve something this client has never heard of?", + requestedSchema: { type: "object", properties: {} } + }, + (reply) => emitMcpDeclineCompletion(state, thread.id, turnId, reply) + ); + break; + } + + if (BEHAVIOR === "mcp-elicitation-null-kind") { + requestServerReply( + "elicitation_" + turnId, + "mcpServer/elicitation/request", + { + threadId: thread.id, + turnId, + serverName: "codegraph", + mode: "form", + _meta: { codex_approval_kind: null }, + message: "Please provide the requested codegraph filter.", + requestedSchema: { type: "object", properties: { filter: { type: "string" } } } + }, + (reply) => emitMcpDeclineCompletion(state, thread.id, turnId, reply) + ); + break; + } + + if (BEHAVIOR === "mcp-elicitation-url-mode") { + requestServerReply( + "elicitation_" + turnId, + "mcpServer/elicitation/request", + { + threadId: thread.id, + turnId, + serverName: "codegraph", + mode: "url", + _meta: { codex_approval_kind: "mcp_tool_call" }, + message: "Finish signing in to codegraph.", + url: "https://example.invalid/oauth", + elicitationId: "elicit_" + turnId + }, + (reply) => emitMcpDeclineCompletion(state, thread.id, turnId, reply) + ); + break; + } + + if (BEHAVIOR === "unknown-server-request") { + requestServerReply( + "unknown_" + turnId, + "unknown/server/request", + { threadId: thread.id, turnId }, + (reply) => { + state.lastUnknownServerRequestReply = reply; + saveState(state); + emitTurnCompleted(thread.id, turnId, { + completed: { + type: "agentMessage", + id: "msg_" + turnId, + text: "Unknown server request ignored.", + phase: "final_answer" + } + }); + } + ); + break; + } + const payload = message.params.outputSchema && message.params.outputSchema.properties && message.params.outputSchema.properties.verdict ? structuredReviewPayload(prompt) : taskPayload(prompt, thread.name && thread.name.startsWith("Codex Companion Task") && prompt.includes("Continue from the current thread state")); diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 8c62a3395..012e4a9f6 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -271,6 +271,131 @@ test("app-server request resolves when the peer replies before the timeout", asy assert.deepEqual(result, { data: [], nextCursor: null }); }); +test("MCP tool approval elicitation is accepted and the tool output reaches the turn", async (t) => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir, "mcp-elicitation-approval"); + + const previousPath = process.env.PATH; + process.env.PATH = buildEnv(binDir).PATH; + t.after(() => { + if (previousPath === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = previousPath; + } + }); + + const result = await runAppServerTurn(repo, { + prompt: "run the codegraph status tool", + sandbox: "read-only" + }); + + assert.equal(result.status, 0, result.error?.message); + assert.equal(result.finalMessage, "MCP tool output: codegraph_status completed."); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.deepEqual(state.lastElicitationReply, { + id: "elicitation_turn_1", + result: { action: "accept", content: {}, _meta: null } + }); +}); + +test("MCP data elicitation without an approval kind is declined", async (t) => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir, "mcp-elicitation-decline"); + + const previousPath = process.env.PATH; + process.env.PATH = buildEnv(binDir).PATH; + t.after(() => { + if (previousPath === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = previousPath; + } + }); + + const result = await runAppServerTurn(repo, { + prompt: "collect the requested filter", + sandbox: "read-only" + }); + + assert.equal(result.status, 0, result.error?.message); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.deepEqual(state.lastElicitationReply, { + id: "elicitation_turn_1", + result: { action: "decline", content: null, _meta: null } + }); +}); + +// Consent is given to exactly one recognized shape. An approval kind this client has never seen, +// a null one, and a url-mode flow all get the same answer as a plain data form. +for (const { behavior, label } of [ + { behavior: "mcp-elicitation-unknown-kind", label: "an unrecognized approval kind" }, + { behavior: "mcp-elicitation-null-kind", label: "a null approval kind" }, + { behavior: "mcp-elicitation-url-mode", label: "a url-mode approval" } +]) { + test(`MCP elicitation carrying ${label} is declined rather than accepted`, async (t) => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir, behavior); + + const previousPath = process.env.PATH; + process.env.PATH = buildEnv(binDir).PATH; + t.after(() => { + if (previousPath === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = previousPath; + } + }); + + const result = await runAppServerTurn(repo, { + prompt: "answer the elicitation", + sandbox: "read-only" + }); + + assert.equal(result.status, 0, result.error?.message); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.deepEqual(state.lastElicitationReply, { + id: "elicitation_turn_1", + result: { action: "decline", content: null, _meta: null } + }); + }); +} + +test("unrelated app-server requests still receive the unsupported-method error", async (t) => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir, "unknown-server-request"); + + const previousPath = process.env.PATH; + process.env.PATH = buildEnv(binDir).PATH; + t.after(() => { + if (previousPath === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = previousPath; + } + }); + + const result = await runAppServerTurn(repo, { + prompt: "ignore the unrelated request", + sandbox: "read-only" + }); + + assert.equal(result.status, 0, result.error?.message); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.deepEqual(state.lastUnknownServerRequestReply, { + id: "unknown_turn_1", + error: { code: -32601, message: "Unsupported server request: unknown/server/request" } + }); +}); + test("app-server connect timeout destroys a client whose initialize never replies", async () => { const workspace = makeTempDir(); const binDir = makeTempDir(); @@ -2369,12 +2494,16 @@ test("write task output focuses on the Codex result without generic follow-up hi assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); -test("write task in linked worktree passes the git common dir as an extra writable root", () => { +test("write task in linked worktree carries the git common dir and disabled MCP servers through start and resume", () => { const repo = makeTempDir(); const worktreeParent = makeTempDir(); const worktree = path.join(worktreeParent, "linked-worktree"); const binDir = makeTempDir(); const statePath = path.join(binDir, "fake-codex-state.json"); + const env = { + ...buildEnv(binDir), + CODEX_DISABLED_MCP_SERVERS: "codegraph,hermes-vault" + }; installFakeCodex(binDir); try { @@ -2384,35 +2513,101 @@ test("write task in linked worktree passes the git common dir as an extra writab run("git", ["commit", "-m", "init"], { cwd: repo }); run("git", ["worktree", "add", "-b", "linked-runtime-test", worktree], { cwd: repo }); - const result = run("node", [SCRIPT, "task", "--write", "fix the failing test"], { - cwd: worktree, - env: buildEnv(binDir) - }); + const result = run("node", [SCRIPT, "task", "--write", "fix the failing test"], { cwd: worktree, env }); assert.equal(result.status, 0, result.stderr); const state = JSON.parse(fs.readFileSync(statePath, "utf8")); assert.equal(state.lastTurnStart.sandboxPolicy?.type, "workspaceWrite"); - assert.deepEqual(state.lastThreadStart.config?.["sandbox_workspace_write.writable_roots"], [ - fs.realpathSync(path.join(repo, ".git")) - ]); - - const resume = run("node", [SCRIPT, "task", "--resume", "--write", "follow up"], { - cwd: worktree, - env: buildEnv(binDir) + assert.deepEqual(state.lastThreadStart.config, { + "mcp_servers.codegraph.enabled": false, + "mcp_servers.hermes-vault.enabled": false, + "sandbox_workspace_write.writable_roots": [fs.realpathSync(path.join(repo, ".git"))] }); + const resume = run("node", [SCRIPT, "task", "--resume", "--write", "follow up"], { cwd: worktree, env }); + assert.equal(resume.status, 0, resume.stderr); const resumedState = JSON.parse(fs.readFileSync(statePath, "utf8")); assert.equal(resumedState.lastTurnStart.sandboxPolicy?.type, "workspaceWrite"); - assert.deepEqual(resumedState.lastThreadResume.config?.["sandbox_workspace_write.writable_roots"], [ - fs.realpathSync(path.join(repo, ".git")) - ]); + assert.deepEqual(resumedState.lastThreadResume.config, { + "mcp_servers.codegraph.enabled": false, + "mcp_servers.hermes-vault.enabled": false, + "sandbox_workspace_write.writable_roots": [fs.realpathSync(path.join(repo, ".git"))] + }); } finally { fs.rmSync(repo, { recursive: true, force: true }); fs.rmSync(worktreeParent, { recursive: true, force: true }); } }); +test("task disables named MCP servers in thread/start config", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const result = run("node", [SCRIPT, "task", "fix the failing test"], { + cwd: repo, + env: { ...buildEnv(binDir), CODEX_DISABLED_MCP_SERVERS: "codegraph,hermes-vault" } + }); + + assert.equal(result.status, 0, result.stderr); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.deepEqual(state.lastThreadStart.config, { + "mcp_servers.codegraph.enabled": false, + "mcp_servers.hermes-vault.enabled": false + }); +}); + +test("task omits MCP config when disabled-server env is unset", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const env = buildEnv(binDir); + delete env.CODEX_DISABLED_MCP_SERVERS; + const result = run("node", [SCRIPT, "task", "fix the failing test"], { cwd: repo, env }); + + assert.equal(result.status, 0, result.stderr); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.equal(state.lastThreadStart.config, undefined); +}); + +test("task trims, deduplicates, and skips invalid disabled MCP server names", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const result = run("node", [SCRIPT, "task", "fix the failing test"], { + cwd: repo, + env: { + ...buildEnv(binDir), + CODEX_DISABLED_MCP_SERVERS: " codegraph , , bad.name , codegraph " + } + }); + + assert.equal(result.status, 0, result.stderr); + const state = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.deepEqual(state.lastThreadStart.config, { + "mcp_servers.codegraph.enabled": false + }); + assert.match(result.stderr, /bad\.name.*bare TOML key/); +}); + test("write task in normal checkout does not add writable root config", () => { const repo = makeTempDir(); const binDir = makeTempDir();