{"record":{"id":"55336440ef4d9cdf","repo":"stablyai/orca","slug":"result-message","errorCode":null,"errorMessage":"result.message","messagePattern":"result\\.message","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/github.ts","lineNumber":184,"sourceCode":"  }\n  if (\n    typeof args !== 'string' &&\n    args.sourceContext?.provider === 'github' &&\n    args.sourceContext.hostId !== getRepoExecutionHostId(repo)\n  ) {\n    return {\n      kind: 'denied',\n      reason: 'host-mismatch',\n      message: 'Access denied: GitHub source host does not match repository host'\n    }\n  }\n  return { kind: 'ok', repo }\n}\n\nfunction assertRegisteredRepo(args: string | RepoScopedArgs, store: Store): Repo {\n  const result = validateRegisteredRepo(args, store)\n  if (result.kind === 'denied') {\n    throw new Error(result.message)\n  }\n  return result.repo\n}\n\nfunction repoConnectionId(repo: Repo): string | null {\n  return repo.connectionId ?? null\n}\n\nfunction localGitOptionArgs(store: Store, repo: Repo): [] | [{ wslDistro?: string }] {\n  const localGitOptions = getLocalProjectWorktreeGitOptions(store, repo)\n  return Object.keys(localGitOptions).length > 0 ? [localGitOptions] : []\n}\n\nfunction applyRepoToPRRefreshCandidate(\n  store: Store,\n  repo: Repo,\n  candidate: GitHubPRRefreshCandidate\n): GitHubPRRefreshCandidate {","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/github.ts#L166-L202","documentation":"Thrown by assertRegisteredRepo (github.ts:181) when validateRegisteredRepo returns a 'denied' result. The thrown message is result.message, which can be one of three: 'Access denied: unknown repository path' (no registered repo matches the path/id), 'Access denied: repository path does not match repo id' (id found but path differs), or 'Access denied: GitHub source host does not match repository host' (sourceContext.hostId differs from the repo's execution host). The 'result.message' label is a static placeholder; the actual string is one of these.","triggerScenarios":"A GitHub IPC handler calls assertRegisteredRepo(args, store) where args is { repoPath, repoId?, sourceContext? }. Denied when: (a) no registered repo matches repoPath (or repoId); (b) repoId is set but the matched repo's path differs from repoPath; (c) sourceContext.provider === 'github' and sourceContext.hostId !== getRepoExecutionHostId(repo).","commonSituations":"Repo was unregistered or moved after a task/PR reference was captured; a task fetched on one machine is replayed on another with a same-path but different-host repo; stale repoId persisted after re-adding a repo; cross-host task sharing without host reconciliation.","solutions":["Re-register the repository so its path/id is in the store, then retry the GitHub IPC call.","If repoId was supplied, drop it and retry by path only, or look up the correct id for the current path.","For host-mismatch, ensure sourceContext.hostId matches the repo's execution host (getRepoExecutionHostId) — do not replay a task captured on host A against a repo on host B.","Sanitize persisted task/PR references to drop repoId and connectionId when the source host changes."],"exampleFix":"// before\nassertRegisteredRepo({ repoPath: savedPath, repoId: savedId, sourceContext }, store)\n\n// after — reconcile before asserting\nconst match = store.getRepos().find((r) => r.id === savedId && resolve(r.path) === resolve(savedPath))\n  ?? store.getRepos().find((r) => resolve(r.path) === resolve(savedPath))\nif (!match) throw new Error('repo not registered; re-add it')\nassertRegisteredRepo({ repoPath: match.path, repoId: match.id, sourceContext: undefined }, store)","handlingStrategy":"validation","validationCode":"// Before any GitHub IPC call: verify the repo is registered and id/path agree\nimport { resolve } from 'node:path'\n\nfunction resolveRegisteredRepoRef(store, { repoPath, repoId, sourceContext }) {\n  const byId = repoId ? store.getRepo(repoId) : undefined\n  const byPath = store.getRepos().find((r) => resolve(r.path) === resolve(repoPath))\n  const repo = byId && byPath && byId.id === byPath.id ? byId : byPath\n  if (!repo) return { kind: 'unregistered' }\n  if (sourceContext?.provider === 'github' && sourceContext.hostId !== getRepoExecutionHostId(repo)) {\n    return { kind: 'host-mismatch' }\n  }\n  return { kind: 'ok', repo }\n}","typeGuard":"export function isRegisteredRepoArgs(\n  args: unknown\n): args is { repoPath: string; repoId?: string | null; sourceContext?: { provider?: string; hostId?: string } | null } {\n  return typeof args === 'object' && args !== null && typeof (args as any).repoPath === 'string'\n}","tryCatchPattern":"try {\n  await ipcRenderer.invoke('gh:something', { repoPath, repoId, sourceContext })\n} catch (e) {\n  const msg = e instanceof Error ? e.message : ''\n  if (msg.startsWith('Access denied: unknown repository')) { promptReRegister(repoPath); return }\n  if (msg.startsWith('Access denied: repository path does not match')) { refreshRepoId(repoPath); return }\n  if (msg.startsWith('Access denied: GitHub source host')) { promptHostRetarget(); return }\n  throw e\n}","preventionTips":["Never persist repoId across host/repo re-registration; re-resolve it by path before reuse.","Strip sourceContext (or align hostId) when intentionally retargeting a task to a different host.","Register repos at canonical resolved paths so symlink drift does not break lookup.","Validate id/path/host consistency at the call site before dispatching GitHub IPC."],"tags":["security","authorization","github","repo-registration","filesystem-auth"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}