mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): distinguish agent and execution harness telemetry (#4071)
This commit is contained in:
@@ -12,6 +12,7 @@ const VENDOR_ENV_KEYS = [
|
||||
"CODEX_CI",
|
||||
"CODEX_SANDBOX_NETWORK_DISABLED",
|
||||
"TERM_PROGRAM",
|
||||
"CURSOR_AGENT",
|
||||
"GITHUB_ACTIONS",
|
||||
"COPILOT_AGENT_ID",
|
||||
"RUNNER_NAME",
|
||||
@@ -55,6 +56,35 @@ describe("detectAgentRuntime — base behavior", () => {
|
||||
expect(detectAgentRuntime()).toBe("claude_code");
|
||||
});
|
||||
|
||||
it("detects Cursor Agent without an IDE terminal marker", async () => {
|
||||
process.env["CURSOR_AGENT"] = "1";
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBe("cursor");
|
||||
process.env["CURSOR_AGENT"] = "";
|
||||
expect(detectAgentRuntime()).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["CLAUDECODE", "claude_code"],
|
||||
["CODEX_THREAD_ID", "codex"],
|
||||
])("keeps child-agent attribution for %s inside Cursor", async (key, runtime) => {
|
||||
process.env["CURSOR_AGENT"] = "1";
|
||||
process.env[key] = "1";
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBe(runtime);
|
||||
});
|
||||
|
||||
it("keeps the Harbor marker separate from agent attribution and never emits its value", async () => {
|
||||
const { detectExecutionHarnessHint, detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectExecutionHarnessHint()).toBeNull();
|
||||
process.env["HARBOR_AGENT"] = "private-harness-value";
|
||||
expect(detectExecutionHarnessHint()).toBe("harbor");
|
||||
expect(detectAgentRuntime()).toBeNull();
|
||||
process.env["CLAUDECODE"] = "1";
|
||||
expect(detectAgentRuntime()).toBe("claude_code");
|
||||
expect(detectExecutionHarnessHint()).toBe("harbor");
|
||||
});
|
||||
|
||||
it("never reads env-var values — even API-key-shaped values stay unread", async () => {
|
||||
process.env["CODEX_THREAD_ID"] = "thread-1";
|
||||
process.env["CODEX_API_KEY"] = "sk-supersecret-DO-NOT-LEAK";
|
||||
@@ -399,6 +429,29 @@ describe("detectAgentHints — new-agent discovery signals", () => {
|
||||
expect(detectAgentHints().agent_env_hints).toContain("FOO_AGENT_SESSION_ID");
|
||||
});
|
||||
|
||||
it("keeps specific markers when infrastructure keys exceed the discovery cap", async () => {
|
||||
for (let i = 0; i < 20; i++) process.env[`AAA_AGENT_${i}_SERVICE_HOST`] = "private-host";
|
||||
process.env["ANTIGRAVITY_AGENT"] = "private-value";
|
||||
process.env["CODEBUDDY_SESSION_ID"] = "private-session";
|
||||
process.env["HARBOR_AGENT"] = "private-harness";
|
||||
const { detectAgentHints } = await import("./agent_runtime.js");
|
||||
const hints = detectAgentHints().agent_env_hints ?? "";
|
||||
expect(hints.split(",")).toHaveLength(16);
|
||||
expect(hints).toContain("ANTIGRAVITY_AGENT");
|
||||
expect(hints).toContain("CODEBUDDY_SESSION_ID");
|
||||
expect(hints).toContain("HARBOR_AGENT");
|
||||
expect(hints).not.toContain("private");
|
||||
expect(hints.split(",")).toEqual(hints.split(",").sort());
|
||||
});
|
||||
|
||||
it("excludes generic npm and login-session keys", async () => {
|
||||
process.env["NPM_CONFIG_USER_AGENT"] = "npm/10";
|
||||
process.env["TERM_SESSION_ID"] = "terminal";
|
||||
process.env["XDG_SESSION_ID"] = "login";
|
||||
const { detectAgentHints } = await import("./agent_runtime.js");
|
||||
expect(detectAgentHints().agent_env_hints).toBeNull();
|
||||
});
|
||||
|
||||
it("clears a full 16-key ambient cap so a fixture key still fits", async () => {
|
||||
for (let i = 0; i < 16; i++) process.env[`AAA_AGENT_${String(i).padStart(2, "0")}`] = "1";
|
||||
process.env["FOO_AGENT_SESSION_ID"] = "x";
|
||||
|
||||
@@ -74,12 +74,12 @@ const VENDOR_RULES: VendorRule[] = [
|
||||
typeof env["CODEX_CI"] === "string" ||
|
||||
typeof env["CODEX_SANDBOX_NETWORK_DISABLED"] === "string",
|
||||
},
|
||||
// Cursor IDE integrated terminal — exports TERM_PROGRAM=cursor (exact,
|
||||
// lowercase). Cursor Background Agent env vars are not publicly documented;
|
||||
// if a canonical marker is identified later, add it here.
|
||||
// Cursor Agent marks its shell with CURSOR_AGENT; TERM_PROGRAM identifies
|
||||
// the IDE terminal only. Keep this after Claude/Codex for nested agents.
|
||||
// Source: https://docs.cursor.com/en/agent/terminal
|
||||
{
|
||||
name: "cursor",
|
||||
check: (env) => env["TERM_PROGRAM"] === "cursor",
|
||||
check: (env) => Boolean(env["CURSOR_AGENT"]) || env["TERM_PROGRAM"] === "cursor",
|
||||
},
|
||||
// Windsurf (Codeium) integrated terminal — exports TERM_PROGRAM=windsurf.
|
||||
// Attested across many independent detectors (nx
|
||||
@@ -244,6 +244,12 @@ export function detectAgentRuntime(): AgentRuntime {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Observed harness marker, not proof of a vendor, benchmark, or human user. */
|
||||
export function detectExecutionHarnessHint(): "harbor" | null {
|
||||
// Read presence only. Preserve this context even when the child agent is known.
|
||||
return typeof process.env["HARBOR_AGENT"] === "string" ? "harbor" : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// New-agent discovery signals.
|
||||
//
|
||||
@@ -345,9 +351,19 @@ export const HINT_KEY_PATTERN = /AGENT|ASSISTANT|COPILOT|CODEX|CLAUDE|LLM|_THREA
|
||||
function isDiscoveryHintKey(upperKey: string): boolean {
|
||||
if (upperKey === "AGENT" || upperKey === "AI_AGENT") return false;
|
||||
if (upperKey.startsWith("SSH_") || upperKey.startsWith("GPG_")) return false;
|
||||
if (["NPM_CONFIG_USER_AGENT", "TERM_SESSION_ID", "XDG_SESSION_ID"].includes(upperKey))
|
||||
return false;
|
||||
return HINT_KEY_PATTERN.test(upperKey);
|
||||
}
|
||||
|
||||
function discoveryHintPriority(key: string): number {
|
||||
// Kubernetes service expansion can otherwise consume the entire hint cap.
|
||||
if (/_SERVICE_(HOST|PORT)(_|$)|_PORT(_\d+_(TCP|UDP)(_(ADDR|PORT|PROTO))?)?$/.test(key)) return 2;
|
||||
if (/^(HARBOR_AGENT|CURSOR_AGENT|ANTIGRAVITY_AGENT)$|_(SESSION_ID|THREAD_ID)$/.test(key))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Residual discovery signals for the agent_runtime=null bucket. See the section
|
||||
* comment above for intent and the privacy contract. Pure over process.env.
|
||||
@@ -363,8 +379,11 @@ export function detectAgentHints(): AgentHints {
|
||||
const upper = key.toUpperCase();
|
||||
if (isDiscoveryHintKey(upper)) keys.add(upper);
|
||||
}
|
||||
const sorted = [...keys].sort();
|
||||
const agent_env_hints = sorted.length ? sorted.slice(0, 16).join(",") : null;
|
||||
const selected = [...keys]
|
||||
.sort((a, b) => discoveryHintPriority(a) - discoveryHintPriority(b) || a.localeCompare(b))
|
||||
.slice(0, 16)
|
||||
.sort();
|
||||
const agent_env_hints = selected.length ? selected.join(",") : null;
|
||||
|
||||
return { agent_hint, term_program, agent_env_hints };
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ vi.mock("node:child_process", () => ({
|
||||
}));
|
||||
|
||||
const { trackEvent, flush, flushSync } = await import("./client.js");
|
||||
const system = await import("./system.js");
|
||||
|
||||
type Batch = { uuid: string; event: string }[];
|
||||
|
||||
@@ -68,6 +69,27 @@ describe("telemetry queue delivery", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("delivers harness context alongside a recognized agent without leaking marker values", async () => {
|
||||
const meta = vi.spyOn(system, "getSystemMeta").mockReturnValue({
|
||||
...system.getSystemMeta(),
|
||||
agent_runtime: "claude_code",
|
||||
execution_harness_hint: "harbor",
|
||||
});
|
||||
try {
|
||||
const fetchMock = vi.fn(() => Promise.resolve(new Response("")));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
trackEvent("cli_command", { command: "skills" });
|
||||
await flush();
|
||||
expect(eventProps(fetchMock)).toMatchObject({
|
||||
agent_runtime: "claude_code",
|
||||
execution_harness_hint: "harbor",
|
||||
});
|
||||
expect(eventProps(fetchMock)).not.toHaveProperty("HARBOR_AGENT");
|
||||
} finally {
|
||||
meta.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("forgets events only after the request completes, and stamps each with a uuid", async () => {
|
||||
const fetchMock = vi.fn(() => Promise.resolve(new Response("")));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
@@ -94,6 +94,7 @@ export function trackEvent(
|
||||
is_tty: sys.is_tty,
|
||||
sandbox_runtime: sys.sandbox_runtime ?? undefined,
|
||||
agent_runtime: sys.agent_runtime ?? undefined,
|
||||
execution_harness_hint: sys.execution_harness_hint ?? undefined,
|
||||
// New-agent discovery signals — populated only when agent_runtime is null.
|
||||
agent_hint: sys.agent_hint ?? undefined,
|
||||
term_program: sys.term_program ?? undefined,
|
||||
@@ -111,8 +112,8 @@ export function trackEvent(
|
||||
// next run: `durable` (loaded from a preexisting config), `unknown`
|
||||
// (minted+persisted this run — an ephemeral HOME is indistinguishable
|
||||
// from a genuine first run), `process_only` (not persisted). Install-
|
||||
// grain metrics should count only durable identities; the identity-
|
||||
// churn workloads (fresh id per run) are never durable.
|
||||
// grain metrics must also consider environment and repeated-day use:
|
||||
// a short-lived container can persist its id between two CLI processes.
|
||||
identity_persistence: getIdentityPersistence(),
|
||||
// Outcome of the identity-establishing config write; absent when that
|
||||
// path did not write (including a durable id loaded from disk).
|
||||
|
||||
@@ -8,6 +8,24 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("getSystemMeta execution context", () => {
|
||||
it("retains a harness marker after the child agent is recognized", async () => {
|
||||
const savedEnv = { ...process.env };
|
||||
try {
|
||||
process.env["CLAUDECODE"] = "1";
|
||||
process.env["HARBOR_AGENT"] = "private-harness-value";
|
||||
const { getSystemMeta } = await import("./system.js");
|
||||
const meta = getSystemMeta();
|
||||
expect(meta.agent_runtime).toBe("claude_code");
|
||||
expect(meta.execution_harness_hint).toBe("harbor");
|
||||
expect(meta.agent_env_hints).toBeNull();
|
||||
expect(JSON.stringify(meta)).not.toContain("private-harness-value");
|
||||
} finally {
|
||||
process.env = savedEnv;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvailableMemoryMb", () => {
|
||||
it("parses vm_stat on macOS to compute available memory", async () => {
|
||||
vi.doMock("node:os", async () => ({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSystemTotalMb } from "@hyperframes/engine";
|
||||
import {
|
||||
detectAgentRuntime,
|
||||
detectAgentHints,
|
||||
detectExecutionHarnessHint,
|
||||
detectSandboxRuntime,
|
||||
type AgentRuntime,
|
||||
type SandboxRuntime,
|
||||
@@ -50,6 +51,8 @@ export interface SystemMeta {
|
||||
* null when no agent is detected.
|
||||
*/
|
||||
agent_runtime: AgentRuntime;
|
||||
/** Observed harness context, independent of agent attribution; null if absent. */
|
||||
execution_harness_hint: "harbor" | null;
|
||||
/**
|
||||
* New-agent discovery signals for the agent_runtime=null bucket, so an agent
|
||||
* we have no rule for surfaces on its own instead of vanishing into null.
|
||||
@@ -95,6 +98,7 @@ export function getSystemMeta(): SystemMeta {
|
||||
is_tty: Boolean(process.stdout?.isTTY),
|
||||
sandbox_runtime: detectSandboxRuntime(),
|
||||
agent_runtime,
|
||||
execution_harness_hint: detectExecutionHarnessHint(),
|
||||
agent_hint: hints.agent_hint,
|
||||
term_program: hints.term_program,
|
||||
agent_env_hints: hints.agent_env_hints,
|
||||
|
||||
Reference in New Issue
Block a user