/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { spawnSync } from 'child_process'; import path from 'path'; import fs from 'fs'; import os from 'os'; import { WorkerProvider, SetupOptions, ExecOptions, SyncOptions, WorkerStatus } from './BaseProvider.ts'; export class GceCosProvider implements WorkerProvider { private projectId: string; private zone: string; private instanceName: string; private sshConfigPath: string; private knownHostsPath: string; private sshAlias = 'gcli-worker'; constructor(projectId: string, zone: string, instanceName: string, repoRoot: string) { this.projectId = projectId; this.zone = zone; this.instanceName = instanceName; const offloadDir = path.join(repoRoot, '.gemini/offload'); if (!fs.existsSync(offloadDir)) fs.mkdirSync(offloadDir, { recursive: true }); this.sshConfigPath = path.join(offloadDir, 'ssh_config'); this.knownHostsPath = path.join(offloadDir, 'known_hosts'); } async provision(): Promise { const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest'; console.log(`šŸš€ Provisioning GCE COS worker: ${this.instanceName}...`); const startupScript = `#!/bin/bash docker pull ${imageUri} docker run -d --name maintainer-worker --restart always \\ -v /home/node/dev:/home/node/dev:rw \\ -v /home/node/.gemini:/home/node/.gemini:rw \\ -v /home/node/.offload:/home/node/.offload:rw \\ ${imageUri} /bin/bash -c "while true; do sleep 1000; done" `; const result = spawnSync('gcloud', [ 'compute', 'instances', 'create', this.instanceName, '--project', this.projectId, '--zone', this.zone, '--machine-type', 'n2-standard-8', '--image-family', 'cos-stable', '--image-project', 'cos-cloud', '--boot-disk-size', '200GB', '--boot-disk-type', 'pd-balanced', '--metadata', `startup-script=${startupScript},enable-oslogin=TRUE`, '--network-interface', 'network=gcli-network,no-address', '--scopes', 'https://www.googleapis.com/auth/cloud-platform' ], { stdio: 'inherit' }); return result.status ?? 1; } async ensureReady(): Promise { const status = await this.getStatus(); if (status.status !== 'RUNNING') { console.log(`āš ļø Worker ${this.instanceName} is ${status.status}. Waking it up...`); const res = spawnSync('gcloud', [ 'compute', 'instances', 'start', this.instanceName, '--project', this.projectId, '--zone', this.zone ], { stdio: 'inherit' }); if (res.status !== 0) return res.status ?? 1; } return 0; } async setup(options: SetupOptions): Promise { const dnsSuffix = options.dnsSuffix || '.internal.gcpnode.com'; // Construct hostname. Restoring verified corporate path requirements: // MUST use 'nic0.' prefix and SHOULD default to '.internal.gcpnode.com' const internalHostname = `nic0.${this.instanceName}.${this.zone}.c.${this.projectId}${dnsSuffix.startsWith('.') ? dnsSuffix : '.' + dnsSuffix}`; const sshEntry = ` Host ${this.sshAlias} HostName ${internalHostname} IdentityFile ~/.ssh/google_compute_engine User ${process.env.USER || 'node'}_google_com UserKnownHostsFile ${this.knownHostsPath} CheckHostIP no StrictHostKeyChecking no ConnectTimeout 5 `; fs.writeFileSync(this.sshConfigPath, sshEntry); console.log(` āœ… Created project SSH config: ${this.sshConfigPath}`); console.log(' - Verifying connection and triggering SSO...'); const directCheck = spawnSync('ssh', ['-F', this.sshConfigPath, this.sshAlias, 'echo 1'], { stdio: 'pipe', shell: true }); if (directCheck.status !== 0) { console.log(' āš ļø Direct internal SSH failed. Attempting IAP tunnel fallback...'); const iapCheck = spawnSync('gcloud', [ 'compute', 'ssh', this.instanceName, '--project', this.projectId, '--zone', this.zone, '--tunnel-through-iap', '--command', 'echo 1' ], { stdio: 'inherit' }); if (iapCheck.status !== 0) { console.error('\nāŒ All connection attempts failed. Please ensure you have "gcert" and IAP permissions.'); return 1; } console.log(' āœ… IAP connection verified.'); } else { console.log(' āœ… Direct internal connection verified.'); } return 0; } async exec(command: string, options: ExecOptions = {}): Promise { const res = await this.getExecOutput(command, options); return res.status; } async getExecOutput(command: string, options: ExecOptions = {}): Promise<{ status: number; stdout: string; stderr: string }> { let finalCmd = command; if (options.wrapContainer) { finalCmd = `docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`; } const sshBase = ['ssh', '-F', this.sshConfigPath, options.interactive ? '-t' : '', this.sshAlias].filter(Boolean); const iapBase = [ 'gcloud', 'compute', 'ssh', this.instanceName, '--project', this.projectId, '--zone', this.zone, '--tunnel-through-iap', '--command' ]; // Try direct first const directRes = spawnSync(sshBase[0], [...sshBase.slice(1), finalCmd], { stdio: options.interactive ? 'inherit' : 'pipe', shell: true }); if (directRes.status === 0) { return { status: 0, stdout: directRes.stdout?.toString() || '', stderr: directRes.stderr?.toString() || '' }; } console.log('āš ļø Direct SSH failed, falling back to IAP...'); const iapRes = spawnSync(iapBase[0], [...iapBase.slice(1), finalCmd], { stdio: options.interactive ? 'inherit' : 'pipe' }); return { status: iapRes.status ?? 1, stdout: iapRes.stdout?.toString() || '', stderr: iapRes.stderr?.toString() || '' }; } async sync(localPath: string, remotePath: string, options: SyncOptions = {}): Promise { const rsyncArgs = ['-avz', '--exclude=".gemini/settings.json"']; if (options.delete) rsyncArgs.push('--delete'); if (options.exclude) { options.exclude.forEach(ex => rsyncArgs.push(`--exclude=${ex}`)); } const sshCmd = `ssh -F ${this.sshConfigPath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=${this.knownHostsPath}`; // Try direct rsync console.log(`šŸ“¦ Syncing ${localPath} to ${this.sshAlias}:${remotePath}...`); const directRes = spawnSync('rsync', [...rsyncArgs, '-e', sshCmd, localPath, `${this.sshAlias}:${remotePath}`], { stdio: 'inherit', shell: true }); if (directRes.status === 0) return 0; console.log('āš ļø Direct rsync failed, falling back to IAP-tunnelled rsync...'); const iapSshCmd = `gcloud compute ssh --project ${this.projectId} --zone ${this.zone} --tunnel-through-iap --quiet`; const iapRes = spawnSync('rsync', [...rsyncArgs, '-e', iapSshCmd, localPath, `${this.instanceName}:${remotePath}`], { stdio: 'inherit', shell: true }); return iapRes.status ?? 1; } async getStatus(): Promise { const res = spawnSync('gcloud', [ 'compute', 'instances', 'describe', this.instanceName, '--project', this.projectId, '--zone', this.zone, '--format', 'json(name,status,networkInterfaces[0].networkIP)' ], { stdio: 'pipe' }); if (res.status !== 0) { return { name: this.instanceName, status: 'UNKNOWN' }; } try { const data = JSON.parse(res.stdout.toString()); return { name: data.name, status: data.status, internalIp: data.networkInterfaces?.[0]?.networkIP }; } catch (e) { return { name: this.instanceName, status: 'ERROR' }; } } async stop(): Promise { const res = spawnSync('gcloud', [ 'compute', 'instances', 'stop', this.instanceName, '--project', this.projectId, '--zone', this.zone ], { stdio: 'inherit' }); return res.status ?? 1; } private quote(str: string) { return `'${str.replace(/'/g, "'\\''")}'`; } }