mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(ssh): let fork-PR worktrees add their contributor remote via the relay Creating a workspace from a fork PR on an SSH host failed with "Destructive git remote operations are not allowed via exec". The relay's git.exec allowlist blocked every `remote` write subcommand, but SSH fork-PR creation has to run `git remote add <fork> <url>` on the host before it can fetch and track the contributor's branch, so the whole create aborted. Allow exactly the two shapes that flow needs -- `remote add <name> <url>` and `remote remove <name>` -- validated with the same remote-name and URL rules the relay already applies to every pushTarget-carrying RPC. Everything else (set-url, rename, prune, extra operands, flags before the action) stays blocked, and the URL must be a github.com clone/ssh URL, so no new reach is granted beyond what push/fetch already accept. `remote remove` was blocked too, which silently leaked fork remotes on SSH hosts: worktree removal swallows the cleanup error. It works again now. A host still running an older relay gets an actionable "reconnect to deploy the latest relay" message instead of the raw policy error. * test(git-exec): pin remote read/write mutation classification Misclassifying `git remote` / `remote get-url` as mutating would flush the relay and SSH provider git read caches on every remote probe, so pin both directions.
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import type { GitPushTarget } from './worktree/types'
|
|
|
|
const SAFE_REMOTE_NAME_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
const GITHUB_CLONE_URL = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/
|
|
const GITHUB_SSH_URL = /^git@github\.com:[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/
|
|
|
|
function assertString(value: unknown, name: string): asserts value is string {
|
|
if (typeof value !== 'string') {
|
|
throw new Error(`Invalid PR push target ${name}.`)
|
|
}
|
|
}
|
|
|
|
export function isSafeGitRemoteName(remoteName: string): boolean {
|
|
if (remoteName.length === 0 || remoteName.length > 100) {
|
|
return false
|
|
}
|
|
return remoteName.split('/').every((segment) => {
|
|
// Git accepts slash-separated remote names; each segment still needs to be
|
|
// a concrete name so persisted push targets cannot smuggle path traversal.
|
|
return (
|
|
segment !== '' &&
|
|
segment !== '.' &&
|
|
segment !== '..' &&
|
|
SAFE_REMOTE_NAME_SEGMENT.test(segment)
|
|
)
|
|
})
|
|
}
|
|
|
|
// Why: the relay allows a fork remote to be added via git.exec, so the exec
|
|
// validator needs the same URL rule the pushTarget-carrying RPCs already apply.
|
|
export function isSafePushTargetRemoteUrl(remoteUrl: string): boolean {
|
|
return GITHUB_CLONE_URL.test(remoteUrl) || GITHUB_SSH_URL.test(remoteUrl)
|
|
}
|
|
|
|
export function assertGitPushTargetShape(target: unknown): asserts target is GitPushTarget {
|
|
if (typeof target !== 'object' || target === null) {
|
|
throw new Error('Invalid PR push target.')
|
|
}
|
|
const candidate = target as Record<string, unknown>
|
|
assertString(candidate.remoteName, 'remote name')
|
|
assertString(candidate.branchName, 'branch name')
|
|
if (!isSafeGitRemoteName(candidate.remoteName)) {
|
|
throw new Error(`Invalid git remote name: ${candidate.remoteName}`)
|
|
}
|
|
if (!candidate.branchName || candidate.branchName.startsWith('-')) {
|
|
throw new Error(`Invalid git branch name: ${candidate.branchName}`)
|
|
}
|
|
if (candidate.remoteUrl !== undefined) {
|
|
assertString(candidate.remoteUrl, 'remote URL')
|
|
if (!isSafePushTargetRemoteUrl(candidate.remoteUrl)) {
|
|
throw new Error('Invalid PR push target remote URL.')
|
|
}
|
|
}
|
|
}
|