diff --git a/.gemini/settings.json b/.gemini/settings.json index 055942be20..1167e60f10 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -12,9 +12,10 @@ "projectId": "gemini-cli-team-quota", "zone": "us-west1-a", "remoteHost": "gcli-worker", - "remoteHome": "/home/node", - "remoteWorkDir": "/home/node/dev/main", - "useContainer": true, + "remoteWorkDir": "~/dev/main", + "userFork": "google-gemini/gemini-cli", + "upstreamRepo": "google-gemini/gemini-cli", + "useContainer": false, "terminalType": "iterm2" } } diff --git a/.gemini/skills/offload/scripts/attach.ts b/.gemini/skills/offload/scripts/attach.ts index c8a161aeb6..952bdb939e 100644 --- a/.gemini/skills/offload/scripts/attach.ts +++ b/.gemini/skills/offload/scripts/attach.ts @@ -33,8 +33,9 @@ export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process } const { remoteHost } = config; + const sshConfigPath = path.join(REPO_ROOT, '.gemini/offload_ssh_config'); const sessionName = `offload-${prNumber}-${action}`; - const finalSSH = `ssh -t ${remoteHost} "tmux attach-session -t ${sessionName}"`; + const finalSSH = `ssh -F ${sshConfigPath} -t ${remoteHost} "tmux attach-session -t ${sessionName}"`; console.log(`šŸ”— Attaching to session: ${sessionName}...`); diff --git a/.gemini/skills/offload/scripts/fleet.ts b/.gemini/skills/offload/scripts/fleet.ts index 004d421d53..55d32dfd61 100644 --- a/.gemini/skills/offload/scripts/fleet.ts +++ b/.gemini/skills/offload/scripts/fleet.ts @@ -133,13 +133,22 @@ async function stopWorker() { async function remoteStatus() { const name = INSTANCE_PREFIX; + const sshConfigPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../offload_ssh_config'); console.log(`šŸ“” Fetching remote status from ${name}...`); - spawnSync('ssh', ['gcli-worker', 'tsx .offload/scripts/status.ts'], { stdio: 'inherit', shell: true }); + spawnSync('ssh', ['-F', sshConfigPath, 'gcli-worker', 'tsx .offload/scripts/status.ts'], { stdio: 'inherit', shell: true }); } async function rebuildWorker() { const name = INSTANCE_PREFIX; console.log(`šŸ”„ Rebuilding worker ${name}...`); + + // Clear isolated known_hosts to prevent ID mismatch on rebuild + const knownHostsPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../offload_known_hosts'); + if (fs.existsSync(knownHostsPath)) { + console.log(` - Clearing isolated known_hosts...`); + fs.unlinkSync(knownHostsPath); + } + spawnSync('gcloud', ['compute', 'instances', 'delete', name, '--project', PROJECT_ID, '--zone', 'us-west1-a', '--quiet'], { stdio: 'inherit' }); await provisionWorker(); } diff --git a/.gemini/skills/offload/scripts/logs.ts b/.gemini/skills/offload/scripts/logs.ts index 4098112aab..918a1ed34f 100644 --- a/.gemini/skills/offload/scripts/logs.ts +++ b/.gemini/skills/offload/scripts/logs.ts @@ -24,6 +24,7 @@ export async function runLogs(args: string[]) { const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); const config = settings.maintainer?.deepReview; const { remoteHost, remoteHome } = config; + const sshConfigPath = path.join(REPO_ROOT, '.gemini/offload_ssh_config'); const jobDir = `${remoteHome}/dev/worktrees/offload-${prNumber}-${action}`; const logDir = `${jobDir}/.gemini/logs`; @@ -41,7 +42,7 @@ export async function runLogs(args: string[]) { tail -f "$latest_log" `; - spawnSync(`ssh ${remoteHost} ${JSON.stringify(tailCmd)}`, { stdio: 'inherit', shell: true }); + spawnSync(`ssh -F ${sshConfigPath} ${remoteHost} ${JSON.stringify(tailCmd)}`, { stdio: 'inherit', shell: true }); return 0; } diff --git a/.gemini/skills/offload/scripts/orchestrator.ts b/.gemini/skills/offload/scripts/orchestrator.ts index 23149d3355..5aa73bc77c 100644 --- a/.gemini/skills/offload/scripts/orchestrator.ts +++ b/.gemini/skills/offload/scripts/orchestrator.ts @@ -47,6 +47,8 @@ export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = p const persistentScripts = `~/.offload/scripts`; const sessionName = `offload-${prNumber}-${action}`; const remoteWorktreeDir = `~/dev/worktrees/${sessionName}`; + const sshConfigPath = path.join(REPO_ROOT, '.gemini/offload_ssh_config'); + const sshBase = `ssh -F ${sshConfigPath}`; // 3. Remote Context Setup (Parallel Worktree) console.log(`šŸš€ Provisioning persistent worktree for ${action} on #${prNumber}...`); @@ -76,7 +78,7 @@ export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = p setupCmd = `docker exec maintainer-worker sh -c ${q(setupCmd)}`; } - spawnSync(`ssh ${remoteHost} ${q(setupCmd)}`, { shell: true, stdio: 'inherit' }); + spawnSync(`${sshBase} ${remoteHost} ${q(setupCmd)}`, { shell: true, stdio: 'inherit' }); // 4. Execution Logic (Persistent Workstation Mode) // We use docker exec if container mode is enabled, otherwise run on host. @@ -92,7 +94,7 @@ export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = p const sshInternal = `tmux attach-session -t ${sessionName} 2>/dev/null || tmux new-session -s ${sessionName} -n 'offload' ${q(tmuxCmd)}`; // High-performance primary SSH with IAP fallback - const finalSSH = `ssh -o ConnectTimeout=5 -t ${remoteHost} ${q(sshInternal)} || gcloud compute ssh ${targetVM} --project ${projectId} --zone ${zone} --tunnel-through-iap --command ${q(sshInternal)}`; + const finalSSH = `${sshBase} -o ConnectTimeout=5 -t ${remoteHost} ${q(sshInternal)} || gcloud compute ssh ${targetVM} --project ${projectId} --zone ${zone} --tunnel-through-iap --command ${q(sshInternal)}`; // 5. Open in iTerm2 const isWithinGemini = !!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID; diff --git a/.gemini/skills/offload/scripts/setup.ts b/.gemini/skills/offload/scripts/setup.ts index 1afa1920e3..43c923910c 100644 --- a/.gemini/skills/offload/scripts/setup.ts +++ b/.gemini/skills/offload/scripts/setup.ts @@ -62,76 +62,40 @@ export async function runSetup(env: NodeJS.ProcessEnv = process.env) { spawnSync(`gcloud compute instances start ${targetVM} --project ${projectId} --zone ${zone}`, { shell: true, stdio: 'inherit' }); } - // 1. Configure Fast-Path SSH Alias (Direct Internal Hostname) - console.log(`\nšŸš€ Configuring Fast-Path SSH Alias (Internal Hostname)...`); + // 1. Configure Isolated SSH Alias (Project-Specific) + console.log(`\nšŸš€ Configuring Isolated SSH Alias...`); const dnsSuffix = await prompt('Internal DNS Suffix (e.g. .internal or .internal.gcpnode.com)', '.internal'); - - // Construct the high-performance direct hostname const internalHostname = `${targetVM}.${zone}.c.${projectId}${dnsSuffix}`; + const sshAlias = 'gcli-worker'; - const sshConfigPath = path.join(os.homedir(), '.ssh/config'); - + const sshConfigPath = path.join(REPO_ROOT, '.gemini/offload_ssh_config'); + const knownHostsPath = path.join(REPO_ROOT, '.gemini/offload_known_hosts'); + const sshEntry = ` -Host ${sshAlias} + Host ${sshAlias} HostName ${internalHostname} IdentityFile ~/.ssh/google_compute_engine User ${env.USER || 'mattkorwel'}_google_com + UserKnownHostsFile ${knownHostsPath} CheckHostIP no StrictHostKeyChecking no -`; + `; + fs.writeFileSync(sshConfigPath, sshEntry); + console.log(` āœ… Created project SSH config: ${sshConfigPath}`); - let currentConfig = ''; - if (fs.existsSync(sshConfigPath)) currentConfig = fs.readFileSync(sshConfigPath, 'utf8'); - - if (!currentConfig.includes(`Host ${sshAlias}`)) { - fs.appendFileSync(sshConfigPath, sshEntry); - console.log(` āœ… Added '${sshAlias}' alias to ~/.ssh/config`); - } - - /* --- Temporarily Skipping Fork Management --- - // 1b. Security Fork Management - console.log('\nšŸ“ Configuring Security Fork...'); - const upstreamRepo = 'google-gemini/gemini-cli'; - - // 1. Robust Discovery using 'gh repo list' - const forksCheck = spawnSync('gh', ['repo', 'list', '--fork', '--limit', '100', '--json', 'nameWithOwner,parent'], { stdio: 'pipe' }); - let existingForks: string[] = []; - try { - const allForks = JSON.parse(forksCheck.stdout.toString()); - existingForks = allForks - .filter((r: any) => r.parent?.nameWithOwner === upstreamRepo) - .map((r: any) => r.nameWithOwner); - } catch (e) {} - - let userFork = ''; - if (existingForks.length > 0) { - console.log(` āœ… Found personal fork: ${existingForks[0]}`); - userFork = existingForks[0]; - } else { - console.log(` šŸ” No personal fork of ${upstreamRepo} found. Creating one...`); - const forkResult = spawnSync('gh', ['repo', 'fork', upstreamRepo, '--clone=false'], { stdio: 'inherit' }); - // Give the API a moment to reflect the new fork - const user = spawnSync('gh', ['api', 'user', '-q', '.login'], { stdio: 'pipe' }).stdout.toString().trim(); - userFork = `${user}/gemini-cli`; - } - - console.log(` šŸ‘‰ Target fork: ${userFork}`); - */ - const userFork = 'google-gemini/gemini-cli'; // Fallback to main repo for now - const upstreamRepo = 'google-gemini/gemini-cli'; - - - // Resolve Paths (Simplified with Tilde) + // Resolve Paths + const sshCmd = `ssh -F ${sshConfigPath}`; const remoteHost = sshAlias; + const remoteHome = '/home/node'; // Hardcoded for our maintainer container const remoteWorkDir = `~/dev/main`; const persistentScripts = `~/.offload/scripts`; console.log(`\nšŸ“¦ Performing One-Time Synchronization...`); - spawnSync(`ssh ${remoteHost} "mkdir -p ${remoteWorkDir} ~/.gemini/policies ${persistentScripts}"`, { shell: true }); - - const rsyncBase = `rsync -avz -e "ssh" --exclude=".gemini/settings.json"`; + // Ensure host directories exist (on the VM Host) + spawnSync(sshCmd, [remoteHost, `mkdir -p ~/dev/main ~/.gemini/policies ~/.offload/scripts`], { shell: true }); + const rsyncBase = `rsync -avz -e "${sshCmd}" --exclude=".gemini/settings.json"`; // 2. Sync Scripts & Policies console.log(' - Pushing offload logic to persistent worker directory...'); spawnSync(`${rsyncBase} --delete .gemini/skills/offload/scripts/ ${remoteHost}:${persistentScripts}/`, { shell: true });