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 <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-10-12 22:16:03 +01:00
committed by GitHub
co-authored by Cursor
parent c3e33835c3
commit b3a9c0cddf
3 changed files with 102 additions and 4 deletions
+1 -1
View File
@@ -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.
+82
View File
@@ -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) {
+19 -3
View File
@@ -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'
}