/ src / utils / background / remote / remoteSession.ts
remoteSession.ts
 1  import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'
 2  import { checkGate_CACHED_OR_BLOCKING } from '../../../services/analytics/growthbook.js'
 3  import { isPolicyAllowed } from '../../../services/policyLimits/index.js'
 4  import { detectCurrentRepositoryWithHost } from '../../detectRepository.js'
 5  import { isEnvTruthy } from '../../envUtils.js'
 6  import type { TodoList } from '../../todo/types.js'
 7  import {
 8    checkGithubAppInstalled,
 9    checkHasRemoteEnvironment,
10    checkIsInGitRepo,
11    checkNeedsClaudeAiLogin,
12  } from './preconditions.js'
13  
14  /**
15   * Background remote session type for managing teleport sessions
16   */
17  export type BackgroundRemoteSession = {
18    id: string
19    command: string
20    startTime: number
21    status: 'starting' | 'running' | 'completed' | 'failed' | 'killed'
22    todoList: TodoList
23    title: string
24    type: 'remote_session'
25    log: SDKMessage[]
26  }
27  
28  /**
29   * Precondition failures for background remote sessions
30   */
31  export type BackgroundRemoteSessionPrecondition =
32    | { type: 'not_logged_in' }
33    | { type: 'no_remote_environment' }
34    | { type: 'not_in_git_repo' }
35    | { type: 'no_git_remote' }
36    | { type: 'github_app_not_installed' }
37    | { type: 'policy_blocked' }
38  
39  /**
40   * Checks eligibility for creating a background remote session
41   * Returns an array of failed preconditions (empty array means all checks passed)
42   *
43   * @returns Array of failed preconditions
44   */
45  export async function checkBackgroundRemoteSessionEligibility({
46    skipBundle = false,
47  }: {
48    skipBundle?: boolean
49  } = {}): Promise<BackgroundRemoteSessionPrecondition[]> {
50    const errors: BackgroundRemoteSessionPrecondition[] = []
51  
52    // Check policy first - if blocked, no need to check other preconditions
53    if (!isPolicyAllowed('allow_remote_sessions')) {
54      errors.push({ type: 'policy_blocked' })
55      return errors
56    }
57  
58    const [needsLogin, hasRemoteEnv, repository] = await Promise.all([
59      checkNeedsClaudeAiLogin(),
60      checkHasRemoteEnvironment(),
61      detectCurrentRepositoryWithHost(),
62    ])
63  
64    if (needsLogin) {
65      errors.push({ type: 'not_logged_in' })
66    }
67  
68    if (!hasRemoteEnv) {
69      errors.push({ type: 'no_remote_environment' })
70    }
71  
72    // When bundle seeding is on, in-git-repo is enough — CCR can seed from
73    // a local bundle. No GitHub remote or app needed. Same gate as
74    // teleport.tsx bundleSeedGateOn.
75    const bundleSeedGateOn =
76      !skipBundle &&
77      (isEnvTruthy(process.env.CCR_FORCE_BUNDLE) ||
78        isEnvTruthy(process.env.CCR_ENABLE_BUNDLE) ||
79        (await checkGate_CACHED_OR_BLOCKING('tengu_ccr_bundle_seed_enabled')))
80  
81    if (!checkIsInGitRepo()) {
82      errors.push({ type: 'not_in_git_repo' })
83    } else if (bundleSeedGateOn) {
84      // has .git/, bundle will work — skip remote+app checks
85    } else if (repository === null) {
86      errors.push({ type: 'no_git_remote' })
87    } else if (repository.host === 'github.com') {
88      const hasGithubApp = await checkGithubAppInstalled(
89        repository.owner,
90        repository.name,
91      )
92      if (!hasGithubApp) {
93        errors.push({ type: 'github_app_not_installed' })
94      }
95    }
96  
97    return errors
98  }