From b3a9c0cddfcf721e4acec19dfbcf3db16e978522 Mon Sep 17 00:00:00 2001 From: HeavyGee <133152184+heavygee@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:16:03 +0100 Subject: [PATCH] fix(cli): strip agent-context env from web Terminal PTYs (#2014) Runner injects HAPI_AGENT_CONTEXT into session CLIs; TerminalManager was forwarding it into human-facing web Terminal shells, so operator tools (lane B auth, stack-switch guards) refused a real person. Strip agent markers, keep HAPI_SESSION_ID, set HAPI_USER_TERMINAL=1. Fixes #2013 Co-authored-by: Cursor --- cli/README.md | 2 +- cli/src/terminal/TerminalManager.test.ts | 82 ++++++++++++++++++++++++ cli/src/terminal/TerminalManager.ts | 22 ++++++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/cli/README.md b/cli/README.md index 11066a7b..11d7e871 100644 --- a/cli/README.md +++ b/cli/README.md @@ -219,7 +219,7 @@ controls for DSH. ### Set for the wrapped agent - `HAPI_SESSION_ID` - The current HAPI session ID, available inside agent shells. Use it in scripts that target the current conversation without listing sessions. -- An explicitly configured `HAPI_API_URL` is also made available to agent shells. HAPI does not copy settings-backed `CLI_API_TOKEN` secrets into the agent environment; credentials already present in the parent environment may still be inherited. Web terminal PTYs strip hub secrets. +- An explicitly configured `HAPI_API_URL` is also made available to agent shells. HAPI does not copy settings-backed `CLI_API_TOKEN` secrets into the agent environment; credentials already present in the parent environment may still be inherited. Web Terminal PTYs (session Terminal button) strip hub secrets and agent-context markers (`HAPI_AGENT_CONTEXT`, `CURSOR_AGENT`, …) and set `HAPI_USER_TERMINAL=1` so operator tools treat that shell as human-facing. For peer discovery and messaging, use the session's MCP `list_peers`, `inspect_peer`, and `ping_peer` tools, or the corresponding CLI commands. diff --git a/cli/src/terminal/TerminalManager.test.ts b/cli/src/terminal/TerminalManager.test.ts index 83821ac4..a4a073ae 100644 --- a/cli/src/terminal/TerminalManager.test.ts +++ b/cli/src/terminal/TerminalManager.test.ts @@ -1,6 +1,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { TerminalManager, + buildFilteredEnv, normalizeTerminalInputForHost, resolveShellCommand } from './TerminalManager' @@ -23,6 +24,87 @@ function setPlatform(value: NodeJS.Platform): void { }) } +describe('buildFilteredEnv web Terminal PTY', () => { + it('strips secrets and agent-context markers and sets HAPI_USER_TERMINAL', () => { + const env = buildFilteredEnv({ + PATH: '/usr/bin', + HOME: '/home/op', + HAPI_SESSION_ID: 'hub-session-1', + HAPI_AGENT_CONTEXT: '1', + CURSOR_AGENT: '1', + CURSOR_AGENT_SESSION_ID: 'cursor-sess', + CLAUDECODE: '1', + CLI_API_TOKEN: 'secret-token', + OPENAI_API_KEY: 'sk-test', + TERM: 'xterm-256color', + }) + + expect(env.HAPI_USER_TERMINAL).toBe('1') + expect(env.HAPI_SESSION_ID).toBe('hub-session-1') + expect(env.PATH).toBe('/usr/bin') + expect(env.HAPI_AGENT_CONTEXT).toBeUndefined() + expect(env.CURSOR_AGENT).toBeUndefined() + expect(env.CURSOR_AGENT_SESSION_ID).toBeUndefined() + expect(env.CLAUDECODE).toBeUndefined() + expect(env.CLI_API_TOKEN).toBeUndefined() + expect(env.OPENAI_API_KEY).toBeUndefined() + }) + + it('passes filtered env into Bun.spawn for a new terminal', () => { + const previousAgentContext = process.env.HAPI_AGENT_CONTEXT + const previousCursorAgent = process.env.CURSOR_AGENT + process.env.HAPI_AGENT_CONTEXT = '1' + process.env.CURSOR_AGENT = '1' + + const terminal = { + write: vi.fn(), + resize: vi.fn(), + close: vi.fn() + } as unknown as Bun.Terminal + const proc = { + terminal, + killed: false, + exitCode: null, + signalCode: null, + kill: vi.fn() + } as unknown as Bun.Subprocess + const spawnMock = vi.fn((_cmd: string[], _opts: { env?: NodeJS.ProcessEnv }) => proc) + const previousBun = globalWithBun.Bun + globalWithBun.Bun = { spawn: spawnMock } + + try { + const manager = new TerminalManager({ + sessionId: 'session-env', + getSessionPath: () => '/tmp/project', + onReady: () => {}, + onOutput: () => {}, + onExit: () => {}, + onError: () => {}, + idleTimeoutMs: 0 + }) + manager.create('term-env', 80, 24) + + expect(spawnMock).toHaveBeenCalled() + const spawnEnv = spawnMock.mock.calls[0]?.[1]?.env + expect(spawnEnv?.HAPI_USER_TERMINAL).toBe('1') + expect(spawnEnv?.HAPI_AGENT_CONTEXT).toBeUndefined() + expect(spawnEnv?.CURSOR_AGENT).toBeUndefined() + } finally { + globalWithBun.Bun = previousBun + if (previousAgentContext === undefined) { + delete process.env.HAPI_AGENT_CONTEXT + } else { + process.env.HAPI_AGENT_CONTEXT = previousAgentContext + } + if (previousCursorAgent === undefined) { + delete process.env.CURSOR_AGENT + } else { + process.env.CURSOR_AGENT = previousCursorAgent + } + } + }) +}) + describe('TerminalManager Windows support', () => { beforeAll(() => { if (!originalPlatformDescriptor?.configurable) { diff --git a/cli/src/terminal/TerminalManager.ts b/cli/src/terminal/TerminalManager.ts index 47594875..7f86c0cc 100644 --- a/cli/src/terminal/TerminalManager.ts +++ b/cli/src/terminal/TerminalManager.ts @@ -38,6 +38,19 @@ const SENSITIVE_ENV_KEYS = new Set([ 'GOOGLE_API_KEY' ]) +/** + * Runner-injected / agent-harness markers. The web Terminal button opens a + * human-facing PTY (not an agent tool shell); inheriting these would make + * operator tools (lane B auth, stack-switch guards) refuse a real person. + * Keep HAPI_SESSION_ID so session-scoped CLI helpers still work. + */ +const AGENT_CONTEXT_ENV_KEYS = new Set([ + 'HAPI_AGENT_CONTEXT', + 'CURSOR_AGENT', + 'CURSOR_AGENT_SESSION_ID', + 'CLAUDECODE', +]) + function getOptionalBun(): typeof Bun | null { return typeof Bun === 'undefined' ? null : Bun } @@ -103,17 +116,20 @@ export function normalizeTerminalInputForHost(data: string): string { return normalized } -function buildFilteredEnv(): NodeJS.ProcessEnv { +/** Env for web Terminal PTYs: strip secrets + agent-context; mark human-facing. */ +export function buildFilteredEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = {} - for (const [key, value] of Object.entries(process.env)) { + for (const [key, value] of Object.entries(source)) { if (!value) { continue } - if (SENSITIVE_ENV_KEYS.has(key)) { + if (SENSITIVE_ENV_KEYS.has(key) || AGENT_CONTEXT_ENV_KEYS.has(key)) { continue } env[key] = value } + // Positive signal for tooling that wants "web Terminal", not agent context. + env.HAPI_USER_TERMINAL = '1' if (!env.TERM) { env.TERM = 'xterm-256color' }