mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
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:
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user