mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(git): keep the agent-terminal credential guard scalar-only so env scrubs can't break git (#38716)
* fix(git): keep the agent-terminal credential guard scalar-only so env scrubs can't break git Agents that scrub env names matching KEY/TOKEN/... (DeepSeek Harness) dropped GIT_CONFIG_KEY_n but kept GIT_CONFIG_COUNT, so every git command in those terminals failed with "missing config key GIT_CONFIG_KEY_0". GCM_INTERACTIVE already carries credential.interactive and suppresses GCM's GUI, so the guard now emits only scalar variables. Tracked in #26594. Co-authored-by: yu.xia <yuxianice@163.com> * chore(git): drop stale indexed-config comments and a dead WSL forward branch --------- Co-authored-by: yu.xia <yuxianice@163.com>
This commit is contained in:
@@ -57,25 +57,6 @@ export function readValidGitConfigEnvCount(env: NodeJS.ProcessEnv): number | nul
|
||||
return hasDanglingIndex ? null : count
|
||||
}
|
||||
|
||||
/** Compose indexed Git config without clobbering caller-provided entries. */
|
||||
export function appendGitConfigEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
entries: readonly (readonly [key: string, value: string])[]
|
||||
): NodeJS.ProcessEnv {
|
||||
const next = { ...env }
|
||||
const base = readValidGitConfigEnvCount(env)
|
||||
if (base === null) {
|
||||
// Ambiguous indices may hold caller data; use scalar guards instead of overwriting them.
|
||||
return next
|
||||
}
|
||||
entries.forEach(([key, value], index) => {
|
||||
next[`GIT_CONFIG_KEY_${base + index}`] = key
|
||||
next[`GIT_CONFIG_VALUE_${base + index}`] = value
|
||||
})
|
||||
next.GIT_CONFIG_COUNT = String(base + entries.length)
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable interactive Git credential UI while preserving cached credentials
|
||||
* and caller-provided askpass programs.
|
||||
@@ -84,23 +65,19 @@ export function gitCredentialPromptGuardEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): NodeJS.ProcessEnv {
|
||||
const next = appendGitConfigEnv(
|
||||
{
|
||||
...env,
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
GIT_ASKPASS: env.GIT_ASKPASS ?? '',
|
||||
SSH_ASKPASS: env.SSH_ASKPASS ?? '',
|
||||
// Why: GCM can ignore terminal/askpass guards and open its own GUI.
|
||||
GCM_INTERACTIVE: 'never'
|
||||
},
|
||||
// Retain cached credentials while disabling only interactive helper fallback.
|
||||
[
|
||||
['credential.interactive', 'false'],
|
||||
['credential.guiPrompt', 'false']
|
||||
]
|
||||
)
|
||||
// Why: scalar guards only. Agents scrub env names matching KEY/TOKEN/..., which drops
|
||||
// GIT_CONFIG_KEY_n but keeps GIT_CONFIG_COUNT, and Git then refuses every command (#26594).
|
||||
// GCM_INTERACTIVE is GCM's env form of credential.interactive and also suppresses its GUI.
|
||||
const next: NodeJS.ProcessEnv = {
|
||||
...env,
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
GIT_ASKPASS: env.GIT_ASKPASS ?? '',
|
||||
SSH_ASKPASS: env.SSH_ASKPASS ?? '',
|
||||
// Why: GCM can ignore terminal/askpass guards and open its own GUI.
|
||||
GCM_INTERACTIVE: 'never'
|
||||
}
|
||||
if (platform === 'win32') {
|
||||
// WSLENV must register the complete indexed config protocol or Git rejects its count.
|
||||
// WSLENV must forward a caller's indexed config as a complete set or Git rejects its count.
|
||||
const configKeys =
|
||||
readValidGitConfigEnvCount(next) === null
|
||||
? []
|
||||
|
||||
@@ -747,19 +747,17 @@ describeBinaryCompatibility('real Git binary compatibility', () => {
|
||||
await runGit(['-C', worktree, 'update-ref', '-d', backupRef, backupOid])
|
||||
})
|
||||
|
||||
it('degrades indexed credential config safely at the Git 2.31 boundary', async () => {
|
||||
const guardEnv = gitCredentialPromptGuardEnv({}, 'linux')
|
||||
await expect(runGit(['status', '--short'], guardEnv)).resolves.toBeDefined()
|
||||
|
||||
try {
|
||||
const result = await runGit(['config', '--get', 'credential.interactive'], guardEnv)
|
||||
expect(supports(2, 31)).toBe(true)
|
||||
expect(result.stdout.trim()).toBe('false')
|
||||
} catch {
|
||||
// Git 2.25 ignores the indexed variables rather than rejecting commands;
|
||||
// the scalar prompt guards still provide the baseline fail-fast behavior.
|
||||
expect(supports(2, 31)).toBe(false)
|
||||
}
|
||||
it('keeps git usable when an agent scrubs credential-shaped env names (#26594)', async () => {
|
||||
// The DeepSeek Harness subprocess filter, recorded in #26594.
|
||||
const sensitiveEnvPattern = /KEY|PASSWORD|SECRET|TOKEN/i
|
||||
const scrubbed = Object.fromEntries(
|
||||
Object.entries(gitCredentialPromptGuardEnv({}, 'linux')).filter(
|
||||
([key]) => !sensitiveEnvPattern.test(key)
|
||||
)
|
||||
)
|
||||
expect(scrubbed.GIT_TERMINAL_PROMPT).toBe('0')
|
||||
expect(scrubbed.GCM_INTERACTIVE).toBe('never')
|
||||
await expect(runGit(['status', '--short'], scrubbed)).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
// Git 2.25–2.49 echoes --end-of-options with --symbolic-full-name but consumes it with --verify; git-history skips the echoed line.
|
||||
|
||||
Reference in New Issue
Block a user