diff --git a/package.json b/package.json index a7ee06676e..cd2029a5a6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "scripts": { "start": "cross-env NODE_ENV=development node scripts/start.js", "start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server", + "start:forever": "node scripts/start-forever.js", + "start:bridge": "node packages/a2a-server/dist/src/chat-bridge/bridge.js", "debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js", "deflake": "node scripts/deflake.js", "deflake:test:integration:sandbox:none": "npm run deflake -- --command=\"npm run test:integration:sandbox:none -- --retry=0\"", diff --git a/packages/cli/src/external-listener.ts b/packages/cli/src/external-listener.ts index 48ab568f6c..2ae7fba072 100644 --- a/packages/cli/src/external-listener.ts +++ b/packages/cli/src/external-listener.ts @@ -393,7 +393,7 @@ export function startExternalListener(options?: { }, ); - server.listen(port, '127.0.0.1', () => { + server.listen(port, '0.0.0.0', () => { const address = server.address(); const actualPort = typeof address === 'object' && address ? address.port : port; diff --git a/scripts/gce-startup.sh b/scripts/gce-startup.sh new file mode 100644 index 0000000000..309a4099a0 --- /dev/null +++ b/scripts/gce-startup.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# GCE startup script — runs on VM boot. +# Installs Node.js, clones, builds, starts forever agent + bridge. +# Pass gemini-api-key via instance metadata. +set -euo pipefail + +LOG="/var/log/forever-agent-startup.log" +exec > >(tee -a "$LOG") 2>&1 +echo "=== Forever Agent startup $(date) ===" + +# Read API key from instance metadata +GEMINI_API_KEY=$(curl -sf -H "Metadata-Flavor: Google" \ + "http://metadata.google.internal/computeMetadata/v1/instance/attributes/gemini-api-key" || echo "") +CHAT_PROJECT_NUMBER=$(curl -sf -H "Metadata-Flavor: Google" \ + "http://metadata.google.internal/computeMetadata/v1/instance/attributes/chat-project-number" || echo "") + +if [ -z "$GEMINI_API_KEY" ]; then + echo "ERROR: gemini-api-key not set in instance metadata" + exit 1 +fi +echo "API key loaded (${#GEMINI_API_KEY} chars)" + +# Install Node.js + screen (first boot only) +if ! command -v node &>/dev/null; then + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs git screen +fi +echo "Node $(node --version)" + +# Clone/update repo +REPO_DIR="/opt/forever-agent" +if [ ! -d "$REPO_DIR" ]; then + GIT_TERMINAL_PROMPT=0 git clone -b afw/forever-gchat \ + https://github.com/google-gemini/gemini-cli.git "$REPO_DIR" +else + cd "$REPO_DIR" && GIT_TERMINAL_PROMPT=0 git pull --ff-only || true +fi + +# npm install && npm run build +cd "$REPO_DIR" +npm install 2>&1 | tail -5 +npm run build 2>&1 | tail -10 + +# Pre-create workspace and config to skip interactive prompts +WORK_DIR="/opt/forever-workspace" +mkdir -p "$WORK_DIR/.gemini" +mkdir -p /root/.gemini + +# Pre-trust the workspace folder (skips "Do you trust this folder?" dialog) +# --yolo doesn't bypass folder trust, so we must pre-configure it +cat > /root/.gemini/trustedFolders.json << 'TRUSTEOF' +{ + "/opt/forever-workspace": "TRUST_FOLDER" +} +TRUSTEOF + +# Disable folder trust + skip session retention dialog +# Write to both user-level AND workspace-level to be safe +for SETTINGS_DIR in /root/.gemini "$WORK_DIR/.gemini"; do + mkdir -p "$SETTINGS_DIR" + cat > "$SETTINGS_DIR/settings.json" << 'SETTINGSEOF' +{ + "security": { + "folderTrust": { + "enabled": false + }, + "auth": { + "selectedType": "gemini-api-key", + "useExternal": true + } + }, + "general": { + "sessionRetention": { + "enabled": true, + "maxAge": "30d", + "warningAcknowledged": true + } + } +} +SETTINGSEOF +done + +cat > "$WORK_DIR/.gemini/GEMINI.md" << 'GEMINIEOF' +--- +sisyphus: + enabled: true + idleTimeout: 30 + prompt: "continue with the next task" +--- + +# Mission +You are a forever-running autonomous agent accessible via Google Chat. +Process incoming tasks, answer questions, and proactively work on improvements. +GEMINIEOF + +# Create systemd service for the chat bridge +cat > /etc/systemd/system/chat-bridge.service << EOF +[Unit] +Description=Google Chat Bridge +After=network.target + +[Service] +Type=simple +Environment=GOOGLE_API_KEY=${GEMINI_API_KEY} +Environment=CHAT_PROJECT_NUMBER=${CHAT_PROJECT_NUMBER} +Environment=A2A_PORT=3100 +Environment=BRIDGE_PORT=8081 +Environment=A2A_URL=http://127.0.0.1:3100 +Environment=GIT_TERMINAL_PROMPT=0 +WorkingDirectory=${REPO_DIR} +ExecStart=/usr/bin/node ${REPO_DIR}/packages/a2a-server/dist/src/chat-bridge/bridge.js +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +# Write env vars to a file — sourced by the wrapper script +cat > /etc/forever-agent.env << ENVEOF +export HOME=/root +export GEMINI_CLI_HOME=/root +export GOOGLE_API_KEY='${GEMINI_API_KEY}' +export GEMINI_API_KEY='${GEMINI_API_KEY}' +export A2A_PORT=3100 +export GIT_TERMINAL_PROMPT=0 +ENVEOF +chmod 600 /etc/forever-agent.env + +# Create a wrapper script that sources env and uses 'script' for pseudo-TTY +cat > /usr/local/bin/start-forever-agent.sh << 'WRAPPEREOF' +#!/usr/bin/env bash +set -a +source /etc/forever-agent.env +set +a +# script provides a pseudo-TTY for Ink; output goes to /dev/null (TUI noise) +exec /usr/bin/script -qfc "node /opt/forever-agent/packages/cli/dist/index.js --forever --a2a-port 3100 --yolo" /dev/null +WRAPPEREOF +chmod +x /usr/local/bin/start-forever-agent.sh + +# Create systemd service for the forever agent +cat > /etc/systemd/system/forever-agent.service << EOF +[Unit] +Description=Gemini CLI Forever Agent +After=network.target chat-bridge.service + +[Service] +Type=simple +Environment=GOOGLE_API_KEY=${GEMINI_API_KEY} +Environment=A2A_PORT=3100 +Environment=GIT_TERMINAL_PROMPT=0 +Environment=HOME=/root +Environment=GEMINI_CLI_HOME=/root +WorkingDirectory=${WORK_DIR} +ExecStart=/usr/local/bin/start-forever-agent.sh +Restart=on-failure +RestartSec=10 +StandardOutput=null +StandardError=journal + +[Install] +WantedBy=multi-user.target +EOF + +# Start both services +systemctl daemon-reload +systemctl enable chat-bridge forever-agent +systemctl start chat-bridge +sleep 2 +systemctl start forever-agent + +echo "=== Chat bridge started on port 8081 ===" +echo "=== Forever agent started in screen session ===" +echo "=== Startup complete $(date) ===" diff --git a/scripts/start-forever.js b/scripts/start-forever.js new file mode 100644 index 0000000000..7e8525d6f9 --- /dev/null +++ b/scripts/start-forever.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(__dirname, '..'); + +const A2A_PORT = process.env.A2A_PORT || '3100'; +const BRIDGE_PORT = process.env.BRIDGE_PORT || '8081'; + +// Ensure .gemini/GEMINI.md exists to skip onboarding dialog +const geminiDir = join(process.cwd(), '.gemini'); +const geminiMd = join(geminiDir, 'GEMINI.md'); +if (!existsSync(geminiMd)) { + mkdirSync(geminiDir, { recursive: true }); + writeFileSync( + geminiMd, + `--- +sisyphus: + enabled: true + idleTimeout: 30 + prompt: "continue with the next task" +--- + +# Mission +You are a forever-running autonomous agent. +Process incoming tasks and answer questions. +`, + ); + console.log(`Created ${geminiMd} (skip onboarding)`); +} + +console.log('=== Gemini CLI Forever Agent ==='); +console.log(`Agent listener: localhost:${A2A_PORT}`); +console.log(`Chat bridge: 0.0.0.0:${BRIDGE_PORT}`); +console.log(''); + +// Start the chat bridge +const bridgePath = join( + repoRoot, + 'packages/a2a-server/dist/src/chat-bridge/bridge.js', +); +const bridge = spawn('node', [bridgePath], { + env: { + ...process.env, + A2A_PORT, + BRIDGE_PORT, + A2A_URL: `http://127.0.0.1:${A2A_PORT}`, + }, + stdio: 'inherit', +}); + +bridge.on('error', (err) => { + console.error(`Bridge failed to start: ${err.message}`); +}); + +// Start the forever agent +const cliPath = join(repoRoot, 'packages/cli/dist/index.js'); +const agent = spawn( + 'node', + [cliPath, '--forever', '--a2a-port', A2A_PORT, '--yolo'], + { + env: { ...process.env, A2A_PORT }, + stdio: 'inherit', + }, +); + +agent.on('error', (err) => { + console.error(`Agent failed to start: ${err.message}`); +}); + +// Cleanup on exit +function cleanup() { + console.log('\nShutting down...'); + bridge.kill(); + agent.kill(); +} + +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +agent.on('exit', (code) => { + console.log(`Agent exited with code ${code}`); + bridge.kill(); + process.exit(code || 0); +}); + +bridge.on('exit', (code) => { + console.log(`Bridge exited with code ${code}`); +});