mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-26 03:33:12 -07:00
feat: GCE forever agent with Cloud Run bridge support
- Add gce-startup.sh: self-contained VM startup (install, build, configure, run) - Add start-forever.js: dual-process launcher for local development - Add npm scripts: start:forever, start:bridge - Bind A2A listener to 0.0.0.0 (was 127.0.0.1) for remote bridge access - Pre-configure all headless settings to bypass interactive dialogs - Use .env file + script pseudo-TTY for systemd service
This commit is contained in:
@@ -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\"",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) ==="
|
||||
@@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user