Enforce native Claude chat identity at launch boundaries (#40548)

This commit is contained in:
Brennan Benson
2026-11-10 10:14:23 -08:00
committed by GitHub
parent fb5a8a0860
commit cec9a6627d
15 changed files with 268 additions and 37 deletions
@@ -0,0 +1,14 @@
id: claude-chat-entrypoint-callers
snapshots:
"import { withOrcaClaudeChatEntrypoint as stamp } from './claude-chat-entrypoint'":
labels:
- source: withOrcaClaudeChatEntrypoint
style: primary
start: 9
end: 37
'withOrcaClaudeChatEntrypoint(env, platform)':
labels:
- source: withOrcaClaudeChatEntrypoint
style: primary
start: 0
end: 28
@@ -0,0 +1,20 @@
id: claude-chat-entrypoint-owner
snapshots:
"const env = { CLAUDE_CODE_ENTRYPOINT: 'orca' }":
labels:
- source: CLAUDE_CODE_ENTRYPOINT
style: primary
start: 14
end: 36
const identity = 'sdk-cli':
labels:
- source: sdk-cli
style: primary
start: 18
end: 25
"env['claude_code_entrypoint'] = 'other'":
labels:
- source: claude_code_entrypoint
style: primary
start: 5
end: 27
@@ -0,0 +1,14 @@
id: claude-entrypoint-key-owner
snapshots:
"env[CLAUDE_ENTRYPOINT_ENV] = 'orca'":
labels:
- source: CLAUDE_ENTRYPOINT_ENV
style: primary
start: 4
end: 25
"import { CLAUDE_ENTRYPOINT_ENV as key } from './claude-chat-entrypoint'":
labels:
- source: CLAUDE_ENTRYPOINT_ENV
style: primary
start: 9
end: 30
@@ -0,0 +1,6 @@
id: claude-chat-entrypoint-callers
valid:
- 'stripInheritedClaudeSessionEnv(env)'
invalid:
- 'withOrcaClaudeChatEntrypoint(env, platform)'
- "import { withOrcaClaudeChatEntrypoint as stamp } from './claude-chat-entrypoint'"
@@ -0,0 +1,8 @@
id: claude-chat-entrypoint-owner
valid:
- 'const env = withOrcaClaudeChatEntrypoint(configured, platform)'
- "const binary = 'orca'"
invalid:
- "const env = { CLAUDE_CODE_ENTRYPOINT: 'orca' }"
- "env['claude_code_entrypoint'] = 'other'"
- "const identity = 'sdk-cli'"
@@ -0,0 +1,6 @@
id: claude-entrypoint-key-owner
valid:
- "const binary = 'orca'"
invalid:
- "env[CLAUDE_ENTRYPOINT_ENV] = 'orca'"
- "import { CLAUDE_ENTRYPOINT_ENV as key } from './claude-chat-entrypoint'"
@@ -0,0 +1,117 @@
id: claude-chat-entrypoint-owner
language: typescript
severity: error
message: Keep the Claude identity literal in claude-chat-entrypoint.ts.
files: ['src/**']
ignores:
- '**/*.test.*'
- '**/*.test-fixtures.*'
- '**/__fixtures__/**'
- 'src/shared/agent-launch/startup/claude-chat-entrypoint.ts'
rule:
all:
- any:
- kind: string_fragment
- kind: property_identifier
- kind: identifier
- regex: '(?i)^(CLAUDE_CODE_ENTRYPOINT|sdk-cli)$'
---
id: claude-chat-entrypoint-owner-tsx
language: tsx
severity: error
message: Keep the Claude identity literal in claude-chat-entrypoint.ts.
files: ['src/**']
ignores:
- '**/*.test.*'
- '**/*.test-fixtures.*'
- '**/__fixtures__/**'
- 'src/shared/agent-launch/startup/claude-chat-entrypoint.ts'
rule:
all:
- any:
- kind: string_fragment
- kind: property_identifier
- kind: identifier
- regex: '(?i)^(CLAUDE_CODE_ENTRYPOINT|sdk-cli)$'
---
id: claude-chat-entrypoint-callers
language: typescript
severity: error
message: Only structured native chat and its own probes may apply the chat identity.
files: ['src/**']
ignores:
- '**/*.test.*'
- '**/*.test-fixtures.*'
- '**/__fixtures__/**'
- 'src/shared/agent-launch/startup/claude-chat-entrypoint.ts'
- 'src/main/claude/claude-stream-json-connection.ts'
- 'src/main/claude/claude-agent-sdk-process-spawn.ts'
- 'src/main/claude/structured/launch/claude-structured-child-env.ts'
- 'src/main/claude/claude-model-catalog-probe.ts'
rule:
all:
- any:
- kind: string_fragment
- kind: property_identifier
- kind: identifier
- regex: '^withOrcaClaudeChatEntrypoint$'
---
id: claude-chat-entrypoint-callers-tsx
language: tsx
severity: error
message: Only structured native chat and its own probes may apply the chat identity.
files: ['src/**']
ignores:
- '**/*.test.*'
- '**/*.test-fixtures.*'
- '**/__fixtures__/**'
- 'src/shared/agent-launch/startup/claude-chat-entrypoint.ts'
- 'src/main/claude/claude-stream-json-connection.ts'
- 'src/main/claude/claude-agent-sdk-process-spawn.ts'
- 'src/main/claude/structured/launch/claude-structured-child-env.ts'
- 'src/main/claude/claude-model-catalog-probe.ts'
rule:
all:
- any:
- kind: string_fragment
- kind: property_identifier
- kind: identifier
- regex: '^withOrcaClaudeChatEntrypoint$'
---
id: claude-entrypoint-key-owner
language: typescript
severity: error
message: Only the chat identity owner and inherited-session isolation may access this key directly.
files: ['src/**']
ignores:
- '**/*.test.*'
- '**/*.test-fixtures.*'
- '**/__fixtures__/**'
- 'src/shared/agent-launch/startup/claude-chat-entrypoint.ts'
- 'src/shared/agent-launch/startup/claude-parent-session-env.ts'
rule:
all:
- any:
- kind: string_fragment
- kind: property_identifier
- kind: identifier
- regex: '^CLAUDE_ENTRYPOINT_ENV$'
---
id: claude-entrypoint-key-owner-tsx
language: tsx
severity: error
message: Only the chat identity owner and inherited-session isolation may access this key directly.
files: ['src/**']
ignores:
- '**/*.test.*'
- '**/*.test-fixtures.*'
- '**/__fixtures__/**'
- 'src/shared/agent-launch/startup/claude-chat-entrypoint.ts'
- 'src/shared/agent-launch/startup/claude-parent-session-env.ts'
rule:
all:
- any:
- kind: string_fragment
- kind: property_identifier
- kind: identifier
- regex: '^CLAUDE_ENTRYPOINT_ENV$'
@@ -90,6 +90,25 @@ describe('claude agent SDK process spawn', () => {
expect(spawn.managed?.stderrTail().length).toBe(8192)
})
it('keeps native chat identity after SDK and configured environment defaults', () => {
const process = fakeSpawn()
createClaudeCodeProcessSpawn(process.spawnImpl, 'win32').spawn(
sdkOptions({
env: {
CLAUDE_CODE_ENTRYPOINT: 'sdk-ts',
claude_code_entrypoint: 'account',
PATH: 'C:\\bin'
}
})
)
expect(process.specs[0]?.env).toMatchObject({ CLAUDE_CODE_ENTRYPOINT: 'orca', PATH: 'C:\\bin' })
expect(
Object.keys(process.specs[0]?.env ?? {}).filter(
(key) => key.toUpperCase() === 'CLAUDE_CODE_ENTRYPOINT'
)
).toEqual(['CLAUDE_CODE_ENTRYPOINT'])
})
it('preserves a Windows .cmd shim and its arguments for the host spawner', () => {
const process = fakeSpawn()
createClaudeCodeProcessSpawn(process.spawnImpl, 'win32').spawn(
@@ -1,3 +1,4 @@
import { withOrcaClaudeChatEntrypoint } from '../../shared/agent-launch/startup/claude-chat-entrypoint'
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import type { PipedProcessSpawner } from '@orca/process-host/process-spec'
import type { SpawnOptions as ClaudeAgentSdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk'
@@ -70,7 +71,7 @@ export function createClaudeCodeProcessSpawn(
{
spawnImpl,
platform,
inheritedEnv: definedEnv(options.env),
inheritedEnv: withOrcaClaudeChatEntrypoint(definedEnv(options.env), platform),
site: 'claude-stream-json-teardown',
policy: (supervised) => claudeChildClosePolicy(supervised, platform),
acceptClose: claudeChildCloseProven
+10 -2
View File
@@ -1,3 +1,4 @@
import { withOrcaClaudeChatEntrypoint } from '../../shared/agent-launch/startup/claude-chat-entrypoint'
import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import {
assertStructuredAgentResolutionCurrent,
@@ -88,7 +89,11 @@ export async function runClaudeListing(
stdin: request.stdin,
...(request.cwd ? { cwd: request.cwd } : {})
},
{ site: request.site, inheritedEnv: env, signal: request.signal }
{
site: request.site,
inheritedEnv: withOrcaClaudeChatEntrypoint(env, process.platform),
signal: request.signal
}
)
}
const result = await runClaudeBackgroundAttempts({
@@ -197,7 +202,10 @@ export function createClaudeModelCatalogProbe(
{
site: 'claude-model-catalog-probe',
// The listing runs from the account folder; relative PATH entries still mean the chat's.
inheritedEnv: launchDirectory ? envWithPathRelativeTo(env, launchDirectory) : env,
inheritedEnv: withOrcaClaudeChatEntrypoint(
launchDirectory ? envWithPathRelativeTo(env, launchDirectory) : env,
process.platform
),
signal,
partialOutput: {
timeoutMs: 2_000,
@@ -1,3 +1,4 @@
import { withOrcaClaudeChatEntrypoint } from '../../shared/agent-launch/startup/claude-chat-entrypoint'
import { claudeStartupAnswerObservation } from './claude-startup-answer-observation'
import type { PipedProcessSpawner } from '@orca/process-host/process-spec'
import type * as ClaudeAgentSdk from '@anthropic-ai/claude-agent-sdk'
@@ -123,7 +124,7 @@ export async function openClaudeStreamJsonConnection(
options: {
...launch.options,
cwd: launch.cwd,
env,
env: withOrcaClaudeChatEntrypoint(env, process.platform),
pathToClaudeCodeExecutable: launch.pathToClaudeCodeExecutable,
spawnClaudeCodeProcess: spawner.spawn,
...(handlers.canUseTool ? { canUseTool: handlers.canUseTool } : {}),
@@ -1,3 +1,4 @@
import { withOrcaClaudeChatEntrypoint } from '../../../../shared/agent-launch/startup/claude-chat-entrypoint'
import { waitForPromiseWithSignal } from '../../../../shared/abort-signal-reason'
import {
assertStructuredAgentResolutionCurrent,
@@ -91,24 +92,6 @@ async function readClaudeChildEnvSources(deps: ClaudeEnvDeps): Promise<ClaudeChi
return { ...invocation, overlay: overlay ? cloneDefinedEnv(overlay) : undefined, inheritedEnv }
}
const CLAUDE_ENTRYPOINT_ENV = 'CLAUDE_CODE_ENTRYPOINT'
// Name Orca as the client instead of the SDK's sdk-ts default or a launching Claude's value.
export function withOrcaClaudeEntrypoint(
inheritedEnv: Record<string, string>,
overlay: Record<string, string> | undefined,
platform: NodeJS.Platform
): Record<string, string> {
const inherited = { ...inheritedEnv }
stripInheritedClaudeSessionEnv(inherited, { platform })
// The SDK reads only the exact name and otherwise adds its own default, which Windows would pick.
return overlayEnvironmentVariables(
{ ...inherited, [CLAUDE_ENTRYPOINT_ENV]: 'orca' },
overlay,
platform
)
}
// Merge onto the shell environment to retain PATH and ambient Anthropic auth.
export function claudeChildEnv(
sources: ClaudeChildEnvSources,
@@ -116,13 +99,9 @@ export function claudeChildEnv(
/** False when the session's seal pairs the CLI runtime itself, behind this app's CLI. */
{ runtimeOnPath = true }: { runtimeOnPath?: boolean } = {}
): Record<string, string> {
const env = decorateEnv(
withOrcaClaudeEntrypoint(
withoutInheritedClaudeConfigDir(sources.inheritedEnv, process.platform),
sources.overlay,
process.platform
)
)
const inherited = withoutInheritedClaudeConfigDir(sources.inheritedEnv, process.platform)
stripInheritedClaudeSessionEnv(inherited)
const env = decorateEnv(overlayEnvironmentVariables(inherited, sources.overlay, process.platform))
return runtimeOnPath
? withCliRuntimeOnPath(sources.command, env, { platform: process.platform })
: env
@@ -130,12 +109,12 @@ export function claudeChildEnv(
// Version probes need the launch runtime and PATH, without auth credentials.
export function claudeProbeEnv(sources: ClaudeChildEnvSources): Record<string, string> {
return applyClaudeEnvPatch(
claudeChildEnv(sources),
{},
{
stripAuthEnv: true,
platform: process.platform
}
return withOrcaClaudeChatEntrypoint(
applyClaudeEnvPatch(
claudeChildEnv(sources),
{},
{ stripAuthEnv: true, platform: process.platform }
),
process.platform
)
}
@@ -0,0 +1,20 @@
import { expect, it } from 'vitest'
import { withOrcaClaudeChatEntrypoint } from './claude-chat-entrypoint'
it('gives the SDK one exact chat identity after differently cased configured values', () => {
expect(
withOrcaClaudeChatEntrypoint(
{
CLAUDE_CODE_ENTRYPOINT: 'sdk-ts',
claude_code_entrypoint: 'configured',
PATH: 'C:\\bin',
CLAUDE_CODE_SESSION_ID: 'deliberate'
},
'win32'
)
).toEqual({
CLAUDE_CODE_ENTRYPOINT: 'orca',
PATH: 'C:\\bin',
CLAUDE_CODE_SESSION_ID: 'deliberate'
})
})
@@ -0,0 +1,16 @@
import { overlayEnvironmentVariables } from '../../environment-overlay'
export const CLAUDE_ENTRYPOINT_ENV = 'CLAUDE_CODE_ENTRYPOINT'
const ORCA_CLAUDE_CHAT_ENTRYPOINT = 'orca'
/** Only structured chats own this identity; other Claude processes retain their CLI default. */
export function withOrcaClaudeChatEntrypoint(
env: Readonly<Record<string, string>>,
platform: NodeJS.Platform
): Record<string, string> {
return overlayEnvironmentVariables(
env,
{ [CLAUDE_ENTRYPOINT_ENV]: ORCA_CLAUDE_CHAT_ENTRYPOINT },
platform
)
}
@@ -1,3 +1,5 @@
import { CLAUDE_ENTRYPOINT_ENV } from './claude-chat-entrypoint'
// A launching Claude's identity and messaging access do not belong to independent processes.
export const CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS = [
'CLAUDECODE',
@@ -9,7 +11,7 @@ export const CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS = [
'CLAUDE_CODE_MESSAGING_SOCKET',
'CLAUDE_CODE_MESSAGING_TOKEN',
'CLAUDE_CODE_EXECPATH',
'CLAUDE_CODE_ENTRYPOINT',
CLAUDE_ENTRYPOINT_ENV,
'CLAUDE_CODE_DESKTOP_APP_VERSION',
'CLAUDE_EFFORT',
'CLAUDE_PID',