mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
Enforce native Claude chat identity at launch boundaries (#40548)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user