diff --git a/packages/cli/src/telemetry/agent_runtime.test.ts b/packages/cli/src/telemetry/agent_runtime.test.ts index 05e449759..bef81385c 100644 --- a/packages/cli/src/telemetry/agent_runtime.test.ts +++ b/packages/cli/src/telemetry/agent_runtime.test.ts @@ -7,28 +7,19 @@ import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; const VENDOR_ENV_KEYS = [ "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", - "CODEX_HOME", - "CODEX_SANDBOX", + "CODEX_THREAD_ID", + "CODEX_CI", "CODEX_SANDBOX_NETWORK_DISABLED", - "CURSOR_TRACE_ID", - "CURSOR_AGENT", "TERM_PROGRAM", "GITHUB_ACTIONS", "COPILOT_AGENT_ID", "RUNNER_NAME", - "JULES_TASK_ID", - "JULES_SESSION", "REPL_ID", "REPLIT_USER", - "DEVIN_SESSION_ID", - "AIDER_RUN_ID", - "GEMINI_CLI", "HERMES_QUIET", - "_HERMES_GATEWAY", - "HERMES_INFERENCE_PROVIDER", - "OPENCLAW_CLI", "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", + "PI_CODING_AGENT", ] as const; function stripVendorEnv(): void { @@ -51,13 +42,13 @@ describe("detectAgentRuntime — base behavior", () => { // Claude Code marker set alongside a Codex marker — Claude Code is the // first rule, so it wins. process.env["CLAUDECODE"] = "1"; - process.env["CODEX_HOME"] = "/home/codex"; + process.env["CODEX_THREAD_ID"] = "thread-1"; const { detectAgentRuntime } = await import("./agent_runtime.js"); expect(detectAgentRuntime()).toBe("claude_code"); }); it("never reads env-var values — even API-key-shaped values stay unread", async () => { - process.env["CODEX_HOME"] = "/home/codex"; + process.env["CODEX_THREAD_ID"] = "thread-1"; process.env["CODEX_API_KEY"] = "sk-supersecret-DO-NOT-LEAK"; const { detectAgentRuntime } = await import("./agent_runtime.js"); const result = detectAgentRuntime(); @@ -94,13 +85,19 @@ describe("detectAgentRuntime — OpenAI Codex", () => { process.env = { ...savedEnv }; }); - it("detects via CODEX_HOME", async () => { - process.env["CODEX_HOME"] = "/home/codex"; + it("detects via CODEX_THREAD_ID (set on every spawned shell command)", async () => { + process.env["CODEX_THREAD_ID"] = "01234567-89ab-cdef-0123-456789abcdef"; const { detectAgentRuntime } = await import("./agent_runtime.js"); expect(detectAgentRuntime()).toBe("codex"); }); - it("detects via CODEX_SANDBOX_NETWORK_DISABLED", async () => { + it("detects via CODEX_CI (hardcoded in UNIFIED_EXEC_ENV)", async () => { + process.env["CODEX_CI"] = "1"; + const { detectAgentRuntime } = await import("./agent_runtime.js"); + expect(detectAgentRuntime()).toBe("codex"); + }); + + it("detects via CODEX_SANDBOX_NETWORK_DISABLED (default-on)", async () => { process.env["CODEX_SANDBOX_NETWORK_DISABLED"] = "1"; const { detectAgentRuntime } = await import("./agent_runtime.js"); expect(detectAgentRuntime()).toBe("codex"); @@ -134,32 +131,20 @@ describe("detectAgentRuntime — Cursor / Copilot / cohort", () => { }); }); -describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", () => { +describe("detectAgentRuntime — Replit / Hermes / openclaw / Pi", () => { const savedEnv = { ...process.env }; beforeEach(stripVendorEnv); afterEach(() => { process.env = { ...savedEnv }; }); - it("detects Jules via JULES_TASK_ID", async () => { - process.env["JULES_TASK_ID"] = "task-1"; - const { detectAgentRuntime } = await import("./agent_runtime.js"); - expect(detectAgentRuntime()).toBe("jules"); - }); - it("detects Replit via REPL_ID", async () => { process.env["REPL_ID"] = "repl-1"; const { detectAgentRuntime } = await import("./agent_runtime.js"); expect(detectAgentRuntime()).toBe("replit"); }); - it("detects Devin via DEVIN_SESSION_ID", async () => { - process.env["DEVIN_SESSION_ID"] = "sess-1"; - const { detectAgentRuntime } = await import("./agent_runtime.js"); - expect(detectAgentRuntime()).toBe("devin"); - }); - - it("detects Hermes via HERMES_QUIET=1 (set unconditionally by cli.py)", async () => { + it("detects Hermes via HERMES_QUIET (set unconditionally by cli.py:50)", async () => { process.env["HERMES_QUIET"] = "1"; const { detectAgentRuntime } = await import("./agent_runtime.js"); expect(detectAgentRuntime()).toBe("hermes"); @@ -170,6 +155,12 @@ describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", () const { detectAgentRuntime } = await import("./agent_runtime.js"); expect(detectAgentRuntime()).toBe("openclaw"); }); + + it("detects Pi via PI_CODING_AGENT (set unconditionally by cli.ts:13)", async () => { + process.env["PI_CODING_AGENT"] = "true"; + const { detectAgentRuntime } = await import("./agent_runtime.js"); + expect(detectAgentRuntime()).toBe("pi"); + }); }); describe("detectSandboxRuntime — file-system path", () => { diff --git a/packages/cli/src/telemetry/agent_runtime.ts b/packages/cli/src/telemetry/agent_runtime.ts index 7ff2d53a0..a146bfd94 100644 --- a/packages/cli/src/telemetry/agent_runtime.ts +++ b/packages/cli/src/telemetry/agent_runtime.ts @@ -26,13 +26,10 @@ export type AgentRuntime = | "codex" | "cursor" | "copilot_agent" - | "jules" | "replit" - | "devin" - | "aider" - | "gemini_cli" | "hermes" | "openclaw" + | "pi" | null; interface VendorRule { @@ -45,64 +42,58 @@ interface VendorRule { // before more generic ones (e.g. copilot_agent before a hypothetical generic // 'github_actions' rule). const VENDOR_RULES: VendorRule[] = [ - // Anthropic Claude Code — local Claude Code spawns subprocesses with - // CLAUDECODE=1 and an entrypoint marker. Same env vars appear in the - // Claude Code Web sandbox. + // Anthropic Claude Code — sets CLAUDECODE=1 on every Bash/PowerShell tool + // spawn (Shell.ts:321) and CLAUDE_CODE_ENTRYPOINT at startup, inherited by + // every child (main.tsx:527). Both propagate to spawned subprocesses. + // Source: confirmed by @magi from Claude Code internal source. { name: "claude_code", - check: (env) => env["CLAUDECODE"] === "1" || typeof env["CLAUDE_CODE_ENTRYPOINT"] === "string", + check: (env) => + typeof env["CLAUDECODE"] === "string" || typeof env["CLAUDE_CODE_ENTRYPOINT"] === "string", }, - // OpenAI Codex — Codex CLI and Codex Cloud both set CODEX_HOME, and the - // managed sandbox additionally sets CODEX_SANDBOX_NETWORK_DISABLED. + // OpenAI Codex (https://github.com/openai/codex). + // - CODEX_THREAD_ID — set unconditionally on every spawned shell command + // (codex-rs/protocol/src/shell_environment.rs:6 constant, set by + // codex-rs/core/src/unified_exec/process_manager.rs:1010 and + // codex-rs/core/src/tools/runtimes/mod.rs:164). + // - CODEX_CI — hardcoded in the UNIFIED_EXEC_ENV array, always set on + // every unified-exec child (process_manager.rs:70). + // - CODEX_SANDBOX_NETWORK_DISABLED — set when network sandbox is active + // (codex-rs/core/src/sandboxing/mod.rs:135-138, default-on). + // CODEX_HOME is deliberately NOT used — it's a config override read at + // Codex startup, not propagated to spawned subprocesses. { name: "codex", check: (env) => - typeof env["CODEX_HOME"] === "string" || - typeof env["CODEX_SANDBOX"] === "string" || + typeof env["CODEX_THREAD_ID"] === "string" || + typeof env["CODEX_CI"] === "string" || typeof env["CODEX_SANDBOX_NETWORK_DISABLED"] === "string", }, - // Cursor IDE + Cursor Background Agents. + // Cursor IDE integrated terminal — exports TERM_PROGRAM=cursor. + // Cursor Background Agent env vars are not publicly documented; if a + // canonical marker is identified later, add it here. { name: "cursor", - check: (env) => - typeof env["CURSOR_TRACE_ID"] === "string" || - typeof env["CURSOR_AGENT"] === "string" || - env["TERM_PROGRAM"] === "cursor", + check: (env) => env["TERM_PROGRAM"] === "cursor", }, - // GitHub Copilot Coding Agent runs inside GitHub Actions, but Copilot's - // workflow injects an extra marker that distinguishes it from generic CI. + // GitHub Copilot Coding Agent — runs inside GitHub Actions and the + // workflow injects an additional marker to distinguish from generic CI. + // Not yet verified from a public-source citation in this audit; the var + // names below match GitHub Copilot Coding Agent documentation but + // should be confirmed before relying on attribution. { name: "copilot_agent", check: (env) => env["GITHUB_ACTIONS"] === "true" && (typeof env["COPILOT_AGENT_ID"] === "string" || env["RUNNER_NAME"] === "Copilot"), }, - // Google Jules. - { - name: "jules", - check: (env) => - typeof env["JULES_TASK_ID"] === "string" || typeof env["JULES_SESSION"] === "string", - }, - // Replit / Replit Agent. + // Replit — REPL_ID and REPLIT_USER are long-documented environment + // variables exposed inside every Replit workspace. + // Source: https://docs.replit.com/replit-workspace/configuring-the-environment { name: "replit", check: (env) => typeof env["REPL_ID"] === "string" || typeof env["REPLIT_USER"] === "string", }, - // Devin (Cognition). - { - name: "devin", - check: (env) => typeof env["DEVIN_SESSION_ID"] === "string", - }, - // Aider. - { - name: "aider", - check: (env) => typeof env["AIDER_RUN_ID"] === "string", - }, - // Gemini CLI — sets a known env var when invoking shell tools. - { - name: "gemini_cli", - check: (env) => typeof env["GEMINI_CLI"] === "string", - }, // Nous Research Hermes Agent — cli.py:50 unconditionally executes // os.environ["HERMES_QUIET"] = "1" // at module load, so the marker propagates via os.environ to every @@ -125,6 +116,14 @@ const VENDOR_RULES: VendorRule[] = [ typeof env["OPENCLAW_STATE_DIR"] === "string" || typeof env["OPENCLAW_CONFIG_PATH"] === "string", }, + // Pi coding agent (https://pi.dev, https://github.com/earendil-works/pi). + // packages/coding-agent/src/cli.ts:13 unconditionally executes + // process.env.PI_CODING_AGENT = "true"; + // at module entry, so every subprocess Pi spawns sees this marker. + { + name: "pi", + check: (env) => typeof env["PI_CODING_AGENT"] === "string", + }, ]; /** diff --git a/packages/cli/src/telemetry/system.ts b/packages/cli/src/telemetry/system.ts index 6f4f5326d..53660ebbe 100644 --- a/packages/cli/src/telemetry/system.ts +++ b/packages/cli/src/telemetry/system.ts @@ -39,9 +39,11 @@ export interface SystemMeta { sandbox_runtime: SandboxRuntime; /** * Coding-agent vendor that spawned this process, if any (claude_code, - * codex, cursor, copilot_agent, jules, replit, devin, aider, gemini_cli). - * Detected by env-var existence only — values are never read. null when - * no agent is detected (i.e. a human invoked the CLI directly). + * codex, cursor, copilot_agent, replit, hermes, openclaw, pi). + * Detected by env-var existence only — values are never read. Every rule + * keys on a marker that has a public-source citation in agent_runtime.ts; + * unverified guesses are deliberately omitted (false-negative > guess). + * null when no agent is detected. */ agent_runtime: AgentRuntime; }