Merge pull request #578 from nwparker/cloud/w6-push-no-prompt

fix(cloud): PUSH-RAW-PROMPT github.com push with no credential never prompts
This commit is contained in:
Neil
2026-11-12 16:13:22 -08:00
committed by GitHub
5 changed files with 158 additions and 8 deletions
File diff suppressed because one or more lines are too long
+69 -4
View File
@@ -2,7 +2,7 @@
# git for agents in Orca Cloud workspaces (first on orcad's PATH). Commands that author or publish
# work run as the teammate whose message started this agent's turn (orca-git-as: orcad attests the
# sender's paired device; root-only member identities). Everything else is plain git, untouched.
real=/usr/bin/git
real="${ORCA_REAL_GIT:-/usr/bin/git}"
sub=''
skip=0
for arg in "$@"; do
@@ -48,18 +48,80 @@ nudge_once() {
local mark="${ORCA_GIT_NUDGE_DIR:-/dev/shm}/orca-git-nudge-$(id -u)"
[ -e "$mark" ] && return 0
: >"$mark" 2>/dev/null || return 0
printf '%s\n' "orca: This commit uses Orca Cloud's name. Connect your GitHub in Settings › Account & billing to use yours." >&2
printf '%s\n' "orca: This commit uses Orca Cloud's name. Connect your GitHub in Settings › Account to use yours." >&2
}
# orca-git-as's own fallback line is replaced by the nudge.
# PUSH-RAW-PROMPT: a push, pull or fetch to github.com that no credential covers (neither the person's
# connected GitHub nor the App installation) must never fall through to git's own "Username for
# 'https://github.com':" prompt. Prompts are off for that remote, and git's auth failure becomes one
# plain line saying what to do.
github_slug=''
github_remote_slug() {
local arg skip=0 seen=0 remote='' url
for arg in "$@"; do
if [ "$skip" = 1 ]; then skip=0; continue; fi
if [ "$seen" = 0 ]; then
case "$arg" in
-C | -c | --git-dir | --work-tree | --namespace | --exec-path | --config-env) skip=1 ;;
"$sub") seen=1 ;;
esac
continue
fi
case "$arg" in
--repo=*) remote="${arg#--repo=}"; break ;;
-o | --push-option | --receive-pack | --upload-pack | --exec | --depth | --deepen | --shallow-since | --shallow-exclude | --refmap | --recurse-submodules-default | -j | --jobs) skip=1 ;;
-*) ;;
*) remote="$arg"; break ;;
esac
done
[ -n "$remote" ] || remote="$("$real" config --get "branch.$("$real" symbolic-ref --short -q HEAD 2>/dev/null).remote" 2>/dev/null || true)"
[ -n "$remote" ] || remote=origin
case "$remote" in
*://*) url="$remote" ;;
*) url="$("$real" remote get-url "$remote" 2>/dev/null || printf '%s' "$remote")" ;;
esac
case "$url" in
https://github.com/* | https://*@github.com/*) url="${url#*github.com/}" ;;
*) return 0 ;;
esac
url="${url%.git}"
url="${url%/}"
case "$url" in */*/*) url="${url%%/*}/$(printf '%s' "${url#*/}" | cut -d/ -f1)" ;; esac
case "$url" in [A-Za-z0-9_.-]*/[A-Za-z0-9_.-]*) github_slug="$url" ;; esac
}
no_access_line() {
local owner="${github_slug%%/*}" verb='push to'
[ "$sub" = push ] || verb='read'
printf '%s\n' "orca: Orca Cloud can't $verb $github_slug. Install the Orca GitHub App on $owner, or connect your GitHub in Settings › Account." >&2
}
# orca-git-as's own fallback line is replaced by the nudge; GitHub auth failures by the plain line.
fallback_filter() {
local line
local line said=0
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
'orca-git-as: no teammate identity'*) nudge_once ;;
*"could not read Username for 'https://github.com'"* | *"could not read Password for 'https://"*"github.com'"* | \
*"Authentication failed for 'https://github.com/"* | 'remote: Permission to '*' denied to '* | \
'remote: Repository not found.'* | "fatal: repository 'https://github.com/"*"' not found"* | \
*"The requested URL returned error: 403"*)
if [ -n "$github_slug" ]; then
[ "$said" = 1 ] || no_access_line
said=1
else
printf '%s\n' "$line"
fi
;;
*) printf '%s\n' "$line" ;;
esac
done
}
case "$sub" in
push | pull | fetch)
github_remote_slug "$@"
if [ -n "$github_slug" ]; then
export GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never
fi
;;
esac
git_as="${ORCA_GIT_AS:-/opt/orca/bin/orca-git-as}"
case "$sub" in
commit | push | merge | rebase | cherry-pick | revert | am | pull | tag)
@@ -72,4 +134,7 @@ case "$sub" in
nudge_once
;;
esac
if [ -n "$github_slug" ]; then
exec "$real" "$@" 2> >(fallback_filter >&2)
fi
exec "$real" "$@"
@@ -0,0 +1,82 @@
// PUSH-RAW-PROMPT: a push, pull or fetch to a github.com repo that no credential covers never reaches git's own
// "Username for 'https://github.com':" prompt. The shim turns prompts off for github.com remotes and replaces
// git's auth failure with one plain line. Other hosts and other failures are untouched.
import { spawnSync } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
const SHIM = fileURLToPath(new URL('../image/vm/git', import.meta.url).href)
let home = ''
let fakeGit = ''
beforeEach(() => {
home = mkdtempSync(join(tmpdir(), 'orca-git-noprompt-'))
mkdirSync(join(home, '.orca-sandbox'))
fakeGit = join(home, 'git')
// A stand-in for /usr/bin/git: answers `remote get-url` from FAKE_REMOTE_URL and fails a network command the
// way real git does with no credential: a prompt when prompts are allowed, the "could not read" error when not.
writeFileSync(
fakeGit,
`#!/usr/bin/env bash
case "$1" in
remote) [ "$2" = get-url ] && { printf '%s\\n' "$FAKE_REMOTE_URL"; exit 0; } ;;
config | symbolic-ref) exit 1 ;;
push | pull | fetch)
echo "GIT_TERMINAL_PROMPT=\${GIT_TERMINAL_PROMPT:-unset}" >&2
if [ "\${GIT_TERMINAL_PROMPT:-}" = 0 ]; then
echo "fatal: could not read Username for 'https://github.com': terminal prompts disabled" >&2
else
printf "Username for 'https://github.com': " >&2
fi
exit 128 ;;
esac
exit 0
`
)
chmodSync(fakeGit, 0o755)
})
afterEach(() => {
rmSync(home, { recursive: true, force: true })
})
function git(args: string[], remoteUrl: string): string {
const result = spawnSync('bash', [SHIM, ...args], {
cwd: home,
env: { PATH: '/usr/bin:/bin', HOME: home, ORCA_REAL_GIT: fakeGit, FAKE_REMOTE_URL: remoteUrl, ORCA_GIT_NUDGE_DIR: home },
encoding: 'utf8'
})
return result.stderr
}
describe('the git shim never lets a github.com push prompt for credentials', () => {
it('push to an uncovered github.com repo: prompts off, one plain line instead of git’s error', () => {
const err = git(['push', 'origin', 'HEAD'], 'https://github.com/octocat/Hello-World.git')
expect(err).toContain('GIT_TERMINAL_PROMPT=0')
expect(err).not.toContain("Username for 'https://github.com'")
expect(err).not.toContain('could not read Username')
expect(err).toContain(
"orca: Orca Cloud can't push to octocat/Hello-World. Install the Orca GitHub App on octocat, or connect your GitHub in Settings › Account."
)
})
it('fetch and pull say "read" and work with the default remote', () => {
expect(git(['fetch'], 'https://github.com/octocat/Hello-World')).toContain("orca: Orca Cloud can't read octocat/Hello-World.")
expect(git(['pull'], 'https://x-access-token@github.com/acme/app.git')).toContain("orca: Orca Cloud can't read acme/app.")
})
it('a push to a non-GitHub remote is untouched (its own prompt, no orca line)', () => {
const err = git(['push', 'origin', 'HEAD'], 'https://gitlab.com/acme/app.git')
expect(err).toContain('GIT_TERMINAL_PROMPT=unset')
expect(err).not.toContain('orca:')
})
it('a push straight to a github.com URL is covered too', () => {
const err = git(['push', 'https://github.com/acme/app', 'HEAD'], '')
expect(err).toContain("orca: Orca Cloud can't push to acme/app.")
})
})
@@ -1,5 +1,5 @@
// #503 follow-up (design-squad): a commit that goes out under the workspace identity (Orca Cloud's
// name) says once per session how to use your own: connect GitHub in Settings › Account & billing.
// name) says once per session how to use your own: connect GitHub in Settings › Account.
// Pushes and read-only git never say it; orca-git-as's own fallback line is replaced by it.
import { spawnSync } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
const SHIM = fileURLToPath(new URL('../image/vm/git', import.meta.url).href)
const NUDGE = "orca: This commit uses Orca Cloud's name. Connect your GitHub in Settings › Account & billing to use yours."
const NUDGE = "orca: This commit uses Orca Cloud's name. Connect your GitHub in Settings › Account to use yours."
let home = ''
let cwd = ''
@@ -57,7 +57,10 @@ describe('resume whose client quits mid-wake', () => {
}
t.background.length = 0
const request = t.h.call('POST', `/v1/sandboxes/${t.id}/resume`)
await new Promise((resolve) => setTimeout(resolve, 10))
// Wait (bounded) for the claim; a fixed sleep raced under full-suite load.
for (let i = 0; i < 200 && t.row().status !== 'resuming'; i++) {
await new Promise((resolve) => setTimeout(resolve, 10))
}
expect(t.row().status).toBe('resuming')
expect(t.background.length).toBeGreaterThan(0)
// The client is gone: only the work handed to waitUntil carries on.