Prevent unscoped GitHub task queries for SSH repos (#2506)

* fix: prevent unscoped ssh github task queries

Co-authored-by: Orca <help@stably.ai>

* test: update worktree card context menu mock

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-05-21 04:23:59 -04:00
committed by GitHub
co-authored by Orca
parent 9adf9058e9
commit fbf53aa4cc
3 changed files with 80 additions and 28 deletions
+54 -27
View File
@@ -779,6 +779,19 @@ type PartialWorkItemsResult = {
issuesError?: ClassifiedError
}
function assertSshRepoHasResolvedGitHubSource(args: {
connectionId?: string | null
issueOwnerRepo: OwnerRepo | null
prOwnerRepo: OwnerRepo | null
}): void {
if (!args.connectionId || args.issueOwnerRepo || args.prOwnerRepo) {
return
}
// Why: SSH repo paths are remote-only, so gh cannot use cwd to infer repo
// context. Without a resolved owner/repo, running gh would query local state.
throw new Error('GitHub work items require a GitHub remote for SSH repositories')
}
async function listRecentWorkItems(
repoPath: string,
issueOwnerRepo: OwnerRepo | null,
@@ -787,7 +800,9 @@ async function listRecentWorkItems(
connectionId?: string | null
): Promise<PartialWorkItemsResult> {
const ghOptions = ghRepoExecOptions(githubRepoContext(repoPath, connectionId))
if (issueOwnerRepo || prOwnerRepo) {
const requiresExplicitRepo = Boolean(connectionId)
assertSshRepoHasResolvedGitHubSource({ connectionId, issueOwnerRepo, prOwnerRepo })
if (issueOwnerRepo || prOwnerRepo || requiresExplicitRepo) {
// Why: allSettled so a 403 on upstream issues doesn't zero out the origin
// PR half — the UI renders partial results plus a banner for the failing
// side, matching the parent design doc's partial-failure rule (§2).
@@ -802,19 +817,21 @@ async function listRecentWorkItems(
],
ghOptions
)
: ghExecFileAsync(
[
'issue',
'list',
'--limit',
String(limit),
'--state',
'open',
'--json',
'number,title,state,url,labels,updatedAt,author'
],
ghOptions
),
: requiresExplicitRepo
? Promise.resolve({ stdout: '[]' })
: ghExecFileAsync(
[
'issue',
'list',
'--limit',
String(limit),
'--state',
'open',
'--json',
'number,title,state,url,labels,updatedAt,author'
],
ghOptions
),
prOwnerRepo
? ghExecFileAsync(
[
@@ -825,19 +842,21 @@ async function listRecentWorkItems(
],
ghOptions
)
: ghExecFileAsync(
[
'pr',
'list',
'--limit',
String(limit),
'--state',
'open',
'--json',
WORK_ITEM_PR_LIST_JSON_FIELDS
],
ghOptions
)
: requiresExplicitRepo
? Promise.resolve({ stdout: '[]' })
: ghExecFileAsync(
[
'pr',
'list',
'--limit',
String(limit),
'--state',
'open',
'--json',
WORK_ITEM_PR_LIST_JSON_FIELDS
],
ghOptions
)
])
let issues: MainWorkItem[] = []
@@ -946,6 +965,8 @@ async function listQueriedWorkItems(
connectionId?: string | null
): Promise<PartialWorkItemsResult> {
const ghOptions = ghRepoExecOptions(githubRepoContext(repoPath, connectionId))
const requiresExplicitRepo = Boolean(connectionId)
assertSshRepoHasResolvedGitHubSource({ connectionId, issueOwnerRepo, prOwnerRepo })
const hasPrOnlyFilter =
query.state === 'merged' ||
query.draft ||
@@ -961,6 +982,9 @@ async function listQueriedWorkItems(
if (!issueScope) {
return { items: [] }
}
if (requiresExplicitRepo && !issueOwnerRepo) {
return { items: [] }
}
const args = buildWorkItemListArgs({
kind: 'issue',
ownerRepo: issueOwnerRepo,
@@ -983,6 +1007,9 @@ async function listQueriedWorkItems(
if (!prScope) {
return []
}
if (requiresExplicitRepo && !prOwnerRepo) {
return []
}
const args = buildWorkItemListArgs({
kind: 'pr',
ownerRepo: prOwnerRepo,