mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-25 22:28:04 +00:00
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:
+1
-1
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user