From 9facfd95f71f203a126669830c278328803b18c0 Mon Sep 17 00:00:00 2001 From: PCBZ Date: Tue, 5 May 2026 21:55:07 -0700 Subject: [PATCH] feat: GCP Compute Engine + Cloudflare R2 persistence, unified config - Add R2 persistence to GCP Compute Engine (rclone restore on boot, 60s periodic sync, shutdown final sync); --exclude openclaw.json so Cloud Run and Compute Engine can share the same bucket for failover - Move openclaw.json template to terraform/shared/openclaw.json.tpl (unified for Cloud Run and Compute Engine); add bonjour_enabled, use_plugin_load_paths, slack_enabled, telegram_owner_id toggles - Move Cloudflare R2 bucket to terraform/shared/cloudflare/ so platform destroy does not delete the bucket - Restrict Telegram DMs to owner only (dmPolicy: allowlist + allowFrom) when telegram_owner_id is set - chmod 600 /root/.openclaw/.env on GCP VM bootstrap Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 4 +- terraform/gcp_cloudrun/.envrc | 1 + terraform/gcp_cloudrun/main.tf | 4 +- terraform/gcp_cloudrun/platform.tf | 6 +- terraform/gcp_cloudrun/provider.tf | 8 -- terraform/gcp_cloudrun/r2.tf | 16 +--- terraform/gcp_cloudrun/rclone-sync.sh | 15 ++-- terraform/gcp_cloudrun/variables.tf | 12 +-- terraform/gcp_vm/.envrc | 4 + terraform/gcp_vm/bootstrap.sh | 80 ++++++++++++++++- terraform/gcp_vm/main.tf | 12 ++- terraform/gcp_vm/openclaw.json.tpl | 90 ------------------- terraform/gcp_vm/terraform.tfvars.example | 16 +++- terraform/gcp_vm/variables.tf | 33 +++++++ terraform/shared/cloudflare/.envrc | 8 ++ terraform/shared/cloudflare/main.tf | 4 + terraform/shared/cloudflare/provider.tf | 13 +++ terraform/shared/cloudflare/variables.tf | 16 ++++ .../openclaw.json.tpl | 23 ++++- 19 files changed, 230 insertions(+), 135 deletions(-) delete mode 100644 terraform/gcp_vm/openclaw.json.tpl create mode 100644 terraform/shared/cloudflare/.envrc create mode 100644 terraform/shared/cloudflare/main.tf create mode 100644 terraform/shared/cloudflare/provider.tf create mode 100644 terraform/shared/cloudflare/variables.tf rename terraform/{gcp_cloudrun => shared}/openclaw.json.tpl (88%) diff --git a/.env.example b/.env.example index c8d1eae..afb39d1 100644 --- a/.env.example +++ b/.env.example @@ -36,4 +36,6 @@ CF_ACCOUNT_ID=your-cloudflare-account-id CF_API_TOKEN=your-cloudflare-api-token # R2 S3 credentials: R2 → Manage R2 API Tokens → Create Account API token R2_ACCESS_KEY_ID=your-r2-access-key-id -R2_SECRET_ACCESS_KEY=your-r2-secret-access-key \ No newline at end of file +R2_SECRET_ACCESS_KEY=your-r2-secret-access-key +# R2 bucket name (shared between Cloud Run and Compute Engine for failover) +R2_BUCKET_NAME=openclaw-state \ No newline at end of file diff --git a/terraform/gcp_cloudrun/.envrc b/terraform/gcp_cloudrun/.envrc index 4851062..e257fa0 100644 --- a/terraform/gcp_cloudrun/.envrc +++ b/terraform/gcp_cloudrun/.envrc @@ -23,3 +23,4 @@ export TF_VAR_cloudflare_account_id="${CF_ACCOUNT_ID:-}" export TF_VAR_cloudflare_api_token="${CF_API_TOKEN:-}" export TF_VAR_r2_access_key_id="${R2_ACCESS_KEY_ID:-}" export TF_VAR_r2_secret_access_key="${R2_SECRET_ACCESS_KEY:-}" +export TF_VAR_r2_bucket_name="${R2_BUCKET_NAME:-}" diff --git a/terraform/gcp_cloudrun/main.tf b/terraform/gcp_cloudrun/main.tf index d9c8c1c..da1a5ee 100644 --- a/terraform/gcp_cloudrun/main.tf +++ b/terraform/gcp_cloudrun/main.tf @@ -39,7 +39,7 @@ resource "google_cloud_run_v2_service" "openclaw" { } env { name = "R2_BUCKET" - value = cloudflare_r2_bucket.state.name + value = var.r2_bucket_name } env { @@ -100,7 +100,7 @@ resource "google_cloud_run_v2_service" "openclaw" { env { name = "HOME" - value = "/tmp/openclaw-state" + value = "/home/node" } env { diff --git a/terraform/gcp_cloudrun/platform.tf b/terraform/gcp_cloudrun/platform.tf index 946a61a..6c63bcf 100644 --- a/terraform/gcp_cloudrun/platform.tf +++ b/terraform/gcp_cloudrun/platform.tf @@ -1,13 +1,17 @@ locals { effective_container_image = "${var.region}-docker.pkg.dev/${var.project_id}/${var.ghcr_remote_repository_id}/${var.ghcr_image_path}:${var.ghcr_image_tag}" - openclaw_json_content = templatefile("${path.module}/openclaw.json.tpl", { + openclaw_json_content = templatefile("${path.module}/../shared/openclaw.json.tpl", { openclaw_gateway_token = var.openclaw_gateway_token openrouter_api_key = var.openrouter_api_key brave_api_key = var.brave_api_key telegram_bot_token = var.telegram_bot_token slack_app_token = var.slack_app_token slack_bot_token = var.slack_bot_token + slack_enabled = true # Cloud Run always provisions Slack secrets + bonjour_enabled = true # Cloud Run: disable bonjour discovery + use_plugin_load_paths = false # Cloud Run: extensions bundled in container image + telegram_owner_id = var.telegram_owner_id }) } diff --git a/terraform/gcp_cloudrun/provider.tf b/terraform/gcp_cloudrun/provider.tf index 2c1cf64..e622abf 100644 --- a/terraform/gcp_cloudrun/provider.tf +++ b/terraform/gcp_cloudrun/provider.tf @@ -5,10 +5,6 @@ terraform { source = "hashicorp/google" version = "~> 6.0" } - cloudflare = { - source = "cloudflare/cloudflare" - version = "~> 5.0" - } null = { source = "hashicorp/null" version = "~> 3.0" @@ -25,7 +21,3 @@ provider "google" { region = var.region credentials = var.gcp_credentials_json != "" ? var.gcp_credentials_json : null } - -provider "cloudflare" { - api_token = var.cloudflare_api_token -} diff --git a/terraform/gcp_cloudrun/r2.tf b/terraform/gcp_cloudrun/r2.tf index 4aa077d..8411210 100644 --- a/terraform/gcp_cloudrun/r2.tf +++ b/terraform/gcp_cloudrun/r2.tf @@ -1,19 +1,11 @@ -resource "cloudflare_r2_bucket" "state" { - account_id = var.cloudflare_account_id - name = var.r2_bucket_name -} +# R2 bucket is managed in terraform/shared/ (independent state). +# Cloud Run only uploads openclaw.json to the existing bucket. -# Write openclaw.json to a local temp file (sensitive: content won't appear in -# Terraform plan/apply output or process listings). resource "local_sensitive_file" "openclaw_json" { content = local.openclaw_json_content filename = "${path.module}/.terraform/tmp/openclaw.json" } -# Upload openclaw.json to R2 via aws CLI (S3-compatible endpoint). -# Reads from the temp file — no secrets are passed as command-line arguments. -# Requires: awscli installed locally. -# rclone-sync sidecar restores it into the container on each startup. resource "null_resource" "openclaw_json_r2" { triggers = { content_hash = md5(local.openclaw_json_content) @@ -22,7 +14,7 @@ resource "null_resource" "openclaw_json_r2" { provisioner "local-exec" { command = <<-EOT aws s3 cp "${local_sensitive_file.openclaw_json.filename}" \ - "s3://${cloudflare_r2_bucket.state.name}/openclaw.json" \ + "s3://${var.r2_bucket_name}/openclaw.json" \ --endpoint-url "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com" \ --content-type "application/json" EOT @@ -33,5 +25,5 @@ resource "null_resource" "openclaw_json_r2" { } } - depends_on = [cloudflare_r2_bucket.state, local_sensitive_file.openclaw_json] + depends_on = [local_sensitive_file.openclaw_json] } diff --git a/terraform/gcp_cloudrun/rclone-sync.sh b/terraform/gcp_cloudrun/rclone-sync.sh index b630955..f903ef5 100644 --- a/terraform/gcp_cloudrun/rclone-sync.sh +++ b/terraform/gcp_cloudrun/rclone-sync.sh @@ -1,10 +1,14 @@ #!/bin/sh set -e +# openclaw.json is platform-specific (Cloud Run vs VM have different plugin config). +# Exclude it from all sync operations so platforms can share the same R2 bucket +# for soul/memory/sessions without overwriting each other's config. +RCLONE_EXCLUDE="--exclude openclaw.json --exclude openclaw.json.bak" + # ── Restore from R2 on startup ──────────────────────────────── -rclone sync r2:$R2_BUCKET/ /data/ --create-empty-src-dirs 2>/dev/null || true +rclone sync r2:$R2_BUCKET/ /data/ --create-empty-src-dirs $RCLONE_EXCLUDE 2>/dev/null || true # Fix permissions: rclone runs as root, openclaw runs as node (uid 1000). -# Make all restored files/dirs world-writable so openclaw can write to them. chmod -R a+rwX /data/ 2>/dev/null || true touch /tmp/rclone-ready echo "rclone: initial restore complete" @@ -15,16 +19,13 @@ echo "rclone: initial restore complete" # ── Final sync on shutdown (SIGTERM) ───────────────────────── cleanup() { echo "rclone: final sync on shutdown..." - # Use copy (not sync) to avoid deleting files written by other instances - rclone copy /data/ r2:$R2_BUCKET/ 2>/dev/null || true + rclone copy /data/ r2:$R2_BUCKET/ --create-empty-src-dirs $RCLONE_EXCLUDE 2>/dev/null || true exit 0 } trap cleanup TERM INT # ── Periodic sync every 60s ─────────────────────────────────── -# Use copy (not sync): sync is destructive and would delete remote files -# written by concurrent instances when max_instances > 1. while true; do sleep 60 - rclone copy /data/ r2:$R2_BUCKET/ 2>/dev/null || true + rclone copy /data/ r2:$R2_BUCKET/ --create-empty-src-dirs $RCLONE_EXCLUDE 2>/dev/null || true done diff --git a/terraform/gcp_cloudrun/variables.tf b/terraform/gcp_cloudrun/variables.tf index c804168..f0a0895 100644 --- a/terraform/gcp_cloudrun/variables.tf +++ b/terraform/gcp_cloudrun/variables.tf @@ -74,6 +74,12 @@ variable "telegram_bot_token" { sensitive = true } +variable "telegram_owner_id" { + description = "Telegram numeric user ID for owner-only DM access. Leave empty for open DMs." + type = string + default = "" +} + variable "openclaw_gateway_token" { type = string sensitive = true @@ -104,12 +110,6 @@ variable "cloudflare_account_id" { type = string } -variable "cloudflare_api_token" { - description = "Cloudflare API Token with R2:Edit permission (for Terraform to create bucket)" - type = string - sensitive = true -} - variable "r2_access_key_id" { description = "R2 S3-compatible Access Key ID (from R2 → Manage R2 API Tokens)" type = string diff --git a/terraform/gcp_vm/.envrc b/terraform/gcp_vm/.envrc index b0e4ce5..b56a31e 100644 --- a/terraform/gcp_vm/.envrc +++ b/terraform/gcp_vm/.envrc @@ -19,3 +19,7 @@ export TF_VAR_brave_api_key="${BRAVE_API_KEY:-}" export TF_VAR_telegram_owner_id="${TELEGRAM_OWNER_ID:-}" export TF_VAR_slack_app_token="${SLACK_APP_TOKEN:-}" export TF_VAR_slack_bot_token="${SLACK_BOT_TOKEN:-}" +export TF_VAR_cloudflare_account_id="${CF_ACCOUNT_ID:-}" +export TF_VAR_r2_access_key_id="${R2_ACCESS_KEY_ID:-}" +export TF_VAR_r2_secret_access_key="${R2_SECRET_ACCESS_KEY:-}" +export TF_VAR_r2_bucket_name="${R2_BUCKET_NAME:-}" diff --git a/terraform/gcp_vm/bootstrap.sh b/terraform/gcp_vm/bootstrap.sh index 1cd300b..2fbff62 100644 --- a/terraform/gcp_vm/bootstrap.sh +++ b/terraform/gcp_vm/bootstrap.sh @@ -44,7 +44,30 @@ curl -fsSL https://openclaw.bot/install.sh | bash -s -- --install-method npm --n npm install -g grammy @grammyjs/runner @grammyjs/transformer-throttler \ @slack/bolt @slack/socket-mode @slack/web-api -# ── 3. Write config ────────────────────────────────────────── +# ── 3. Install rclone + restore from R2 ───────────────────── +%{ if r2_bucket_name != "" ~} +curl https://rclone.org/install.sh | bash + +mkdir -p /root/.config/rclone +cat > /root/.config/rclone/rclone.conf << 'RCLONEEOF' +[r2] +type = s3 +provider = Cloudflare +access_key_id = ${r2_access_key_id} +secret_access_key = ${r2_secret_access_key} +endpoint = https://${cloudflare_account_id}.r2.cloudflarestorage.com +RCLONEEOF + +echo "Restoring OpenClaw state from R2 (${r2_bucket_name})..." +# Exclude openclaw.json: each platform maintains its own config so the same +# R2 bucket can be shared between Cloud Run and Compute Engine for failover. +rclone sync r2:${r2_bucket_name}/ /root/.openclaw/ --create-empty-src-dirs \ + --exclude "openclaw.json" --exclude "openclaw.json.bak" 2>/dev/null || true +chmod -R a+rX /root/.openclaw/ 2>/dev/null || true +echo "R2 restore complete" +%{ endif ~} + +# ── 4. Write config ────────────────────────────────────────── mkdir -p /root/.openclaw # Write injected openclaw.json (provided by Terraform) @@ -68,15 +91,22 @@ fi cat > /root/.openclaw/.env << 'ENVEOF' OPENROUTER_API_KEY=${openrouter_api_key} TELEGRAM_BOT_TOKEN=${telegram_bot_token} +TELEGRAM_OWNER_ID=${telegram_owner_id} OPENCLAW_GATEWAY_TOKEN=${openclaw_gateway_token} BRAVE_API_KEY=${brave_api_key} +SLACK_APP_TOKEN=${slack_app_token} +SLACK_BOT_TOKEN=${slack_bot_token} OPENCLAW_ONBOARD_NON_INTERACTIVE=1 ENVEOF +chmod 600 /root/.openclaw/.env export OPENROUTER_API_KEY=${openrouter_api_key} export TELEGRAM_BOT_TOKEN=${telegram_bot_token} +export TELEGRAM_OWNER_ID=${telegram_owner_id} export OPENCLAW_GATEWAY_TOKEN=${openclaw_gateway_token} export BRAVE_API_KEY=${brave_api_key} +export SLACK_APP_TOKEN=${slack_app_token} +export SLACK_BOT_TOKEN=${slack_bot_token} # ── 5. Onboard ─────────────────────────────────────────────── openclaw doctor --fix || true @@ -120,7 +150,50 @@ OVERRIDEEOF systemctl --user daemon-reload systemctl --user restart openclaw-gateway.service -# ── 7. Auto-approve operator.approvals scope ───────────────── +# ── 7. Setup R2 periodic sync ──────────────────────────────── +%{ if r2_bucket_name != "" ~} +# Periodic sync service: copies /root/.openclaw → R2 every 60s +cat > /etc/systemd/system/openclaw-r2-sync.service << 'R2SYNCEOF' +[Unit] +Description=OpenClaw R2 state periodic sync +After=network.target + +[Service] +Type=simple +Restart=always +RestartSec=5 +ExecStart=/bin/sh -c 'while true; do rclone copy /root/.openclaw/ r2:${r2_bucket_name}/ --create-empty-src-dirs --exclude "openclaw.json" --exclude "openclaw.json.bak" 2>/dev/null; sleep 60; done' + +[Install] +WantedBy=multi-user.target +R2SYNCEOF + +# Final sync on shutdown: ensures no state is lost on VM stop/reboot +cat > /etc/systemd/system/openclaw-r2-final.service << 'R2FINALEOF' +[Unit] +Description=OpenClaw R2 final sync on shutdown +DefaultDependencies=no +Before=shutdown.target reboot.target halt.target +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/rclone copy /root/.openclaw/ r2:${r2_bucket_name}/ --create-empty-src-dirs --exclude "openclaw.json" --exclude "openclaw.json.bak" +TimeoutStartSec=30 +RemainAfterExit=yes + +[Install] +WantedBy=halt.target reboot.target shutdown.target +R2FINALEOF + +systemctl daemon-reload +systemctl enable openclaw-r2-sync.service +systemctl start openclaw-r2-sync.service +systemctl enable openclaw-r2-final.service +echo "R2 sync configured: r2:${r2_bucket_name} (60s interval + shutdown hook)" +%{ endif ~} + +# ── 8. Auto-approve operator.approvals scope ───────────────── echo "Waiting for approval requests..." sleep 120 @@ -178,5 +251,8 @@ LOGROTATEEOF echo "=== GCP Bootstrap Complete ===" echo "Swap: $${swap_size_gb}GB at $${swap_path}" echo "Memory Limit: ${openclaw_memory_limit_mb}MB (systemd cgroup)" +%{ if r2_bucket_name != "" ~} +echo "R2 Persistence: r2:${r2_bucket_name} (60s sync + shutdown hook)" +%{ endif ~} echo "OpenClaw Web: http://localhost:18789/health (test locally)" echo "Check systemd: systemctl --user status openclaw-gateway" diff --git a/terraform/gcp_vm/main.tf b/terraform/gcp_vm/main.tf index 6420bbf..8a6e8ed 100644 --- a/terraform/gcp_vm/main.tf +++ b/terraform/gcp_vm/main.tf @@ -17,7 +17,7 @@ locals { if length(regexall(":", cidr)) > 0 ] - openclaw_json_content = templatefile("${path.module}/openclaw.json.tpl", { + openclaw_json_content = templatefile("${path.module}/../shared/openclaw.json.tpl", { openclaw_gateway_token = var.openclaw_gateway_token openrouter_api_key = var.openrouter_api_key brave_api_key = var.brave_api_key @@ -25,17 +25,27 @@ locals { slack_app_token = var.slack_app_token slack_bot_token = var.slack_bot_token slack_enabled = var.slack_app_token != "" && var.slack_bot_token != "" + bonjour_enabled = false # VM: no bonjour plugin needed + use_plugin_load_paths = true # VM: load extensions from npm global path + telegram_owner_id = var.telegram_owner_id }) bootstrap_vars = { openrouter_api_key = var.openrouter_api_key telegram_bot_token = var.telegram_bot_token + telegram_owner_id = var.telegram_owner_id openclaw_gateway_token = var.openclaw_gateway_token brave_api_key = var.brave_api_key + slack_app_token = var.slack_app_token + slack_bot_token = var.slack_bot_token swap_size = var.swap_size openclaw_memory_limit_mb = var.openclaw_memory_limit_mb approve_operator_script = file("${path.module}/approve_operator_approvals.py") openclaw_json_content = local.openclaw_json_content + cloudflare_account_id = var.cloudflare_account_id + r2_access_key_id = var.r2_access_key_id + r2_secret_access_key = var.r2_secret_access_key + r2_bucket_name = var.r2_bucket_name } } diff --git a/terraform/gcp_vm/openclaw.json.tpl b/terraform/gcp_vm/openclaw.json.tpl deleted file mode 100644 index 114d76d..0000000 --- a/terraform/gcp_vm/openclaw.json.tpl +++ /dev/null @@ -1,90 +0,0 @@ -{ - "gateway": { - "bind": "lan", - "auth": { "mode": "token", "token": "${openclaw_gateway_token}" }, - "mode": "local", - "remote": { "token": "${openclaw_gateway_token}" } - }, - "agents": { - "defaults": { - "model": { - "primary": "openrouter/openai/gpt-4o-mini", - "fallbacks": [ - "openrouter/anthropic/claude-haiku-4.5", - "openrouter/meta-llama/llama-3.3-70b-instruct:free", - "openrouter/auto" - ] - }, - "models": { - "openrouter/anthropic/claude-opus-4.6": {"alias": "opus"}, - "openrouter/anthropic/claude-sonnet-4.6": {"alias": "sonnet"}, - "openrouter/anthropic/claude-haiku-4.5": {"alias": "haiku"}, - "openrouter/openai/gpt-5.4": {"alias": "gpt5"}, - "openrouter/openai/gpt-4o": {"alias": "gpt4o"}, - "openrouter/openai/gpt-4o-mini": {"alias": "mini"}, - "openrouter/google/gemini-2.5-pro": {"alias": "gemini-pro"}, - "openrouter/google/gemini-2.5-flash": {"alias": "flash"}, - "openrouter/deepseek/deepseek-r1": {"alias": "r1"}, - "openrouter/mistralai/devstral-small": {"alias": "devstral"}, - "openrouter/meta-llama/llama-3.3-70b-instruct:free": {"alias": "llama"}, - "openrouter/nvidia/nemotron-3-super-120b-a12b:free": {"alias": "nemotron"}, - "openrouter/qwen/qwen3-coder:free": {"alias": "coder"}, - "openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition:free": {"alias": "uncensored"}, - "openrouter/auto": {"alias": "auto"} - }, - "compaction": { "mode": "safeguard", "reserveTokensFloor": 4000 } - } - }, - "tools": { - "web": { - "search": { "enabled": true, "provider": "brave" }, - "fetch": { - "enabled": true, - "strip_images": true, - "strip_videos": true, - "strip_css": true, - "strip_fonts": true - } - }, - "deny": ["browser"] - }, - "plugins": { - "load": { - "paths": [ - "/usr/lib/node_modules/openclaw/dist/extensions/telegram"%{ if slack_enabled }, - "/usr/lib/node_modules/openclaw/dist/extensions/slack"%{ endif } - ] - }, - "entries": { - "telegram": { "enabled": true }, -%{ if slack_enabled } - "slack": { "enabled": true }, -%{ endif } - "openrouter": { "enabled": true }, - "brave": { - "enabled": true, - "config": { "webSearch": { "apiKey": "${brave_api_key}" } } - } - } - }, - "channels": { - "telegram": { - "enabled": true, - "accounts": { - "default": { - "botToken": "${telegram_bot_token}", - "dmPolicy": "open", - "groupPolicy": "open" - } - } - }%{ if slack_enabled }, - "slack": { - "enabled": true, - "mode": "socket", - "appToken": "${slack_app_token}", - "botToken": "${slack_bot_token}", - "dmPolicy": "open", - "groupPolicy": "open" - }%{ endif } - } -} diff --git a/terraform/gcp_vm/terraform.tfvars.example b/terraform/gcp_vm/terraform.tfvars.example index 1c5fd48..9d9f6ec 100644 --- a/terraform/gcp_vm/terraform.tfvars.example +++ b/terraform/gcp_vm/terraform.tfvars.example @@ -33,4 +33,18 @@ gateway_allowed_cidrs = ["203.0.113.10/32"] # ── Runtime Limits ────────────────────────────────────────── swap_size = 3 -openclaw_memory_limit_mb = 800 +openclaw_memory_limit_mb = 800 # e2-micro: 800MB; e2-small: 1800MB + +# ── Optional secrets (set in .env, auto-loaded via .envrc) ── +# telegram_owner_id = "" # Numeric Telegram user ID → grants /model and admin commands +# slack_app_token = "" # xapp-... Socket Mode token (leave empty to disable Slack) +# slack_bot_token = "" # xoxb-... Bot OAuth token + +# ── Cloudflare R2 (optional persistent memory) ────────────── +# Leave all empty to run without R2. When set, soul/memory/sessions survive VM reboots. +# Account ID: Cloudflare Dashboard → right sidebar +# cloudflare_account_id = "your-cloudflare-account-id" +# R2 S3 credentials: R2 → Manage R2 API Tokens → Create Account API Token +# r2_access_key_id = "your-r2-access-key-id" +# r2_secret_access_key = "your-r2-secret-access-key" +# r2_bucket_name = "openclaw-state-vm" diff --git a/terraform/gcp_vm/variables.tf b/terraform/gcp_vm/variables.tf index 0d4dd85..d3f8b5c 100644 --- a/terraform/gcp_vm/variables.tf +++ b/terraform/gcp_vm/variables.tf @@ -82,6 +82,12 @@ variable "openclaw_memory_limit_mb" { default = 800 } +variable "telegram_owner_id" { + description = "Telegram numeric user ID for privileged commands e.g. /model (optional)" + type = string + default = "" +} + variable "openrouter_api_key" { type = string sensitive = true @@ -117,3 +123,30 @@ variable "slack_bot_token" { sensitive = true default = "" } + +# ── Cloudflare R2 (optional persistent memory) ────────────── +variable "cloudflare_account_id" { + description = "Cloudflare Account ID (for R2 persistence). Leave empty to disable R2." + type = string + default = "" +} + +variable "r2_access_key_id" { + description = "R2 S3-compatible Access Key ID. Leave empty to disable R2." + type = string + sensitive = true + default = "" +} + +variable "r2_secret_access_key" { + description = "R2 S3-compatible Secret Access Key. Leave empty to disable R2." + type = string + sensitive = true + default = "" +} + +variable "r2_bucket_name" { + description = "R2 bucket name for OpenClaw state persistence. Leave empty to disable R2." + type = string + default = "" +} diff --git a/terraform/shared/cloudflare/.envrc b/terraform/shared/cloudflare/.envrc new file mode 100644 index 0000000..5ca198b --- /dev/null +++ b/terraform/shared/cloudflare/.envrc @@ -0,0 +1,8 @@ +# Auto-load secrets from .env into Terraform variables +# Requires: brew install direnv && eval "$(direnv hook zsh)" >> ~/.zshrc + +dotenv ../../../.env + +export TF_VAR_cloudflare_account_id="${CF_ACCOUNT_ID:-}" +export TF_VAR_cloudflare_api_token="${CF_API_TOKEN:-}" +export TF_VAR_r2_bucket_name="${R2_BUCKET_NAME:-}" diff --git a/terraform/shared/cloudflare/main.tf b/terraform/shared/cloudflare/main.tf new file mode 100644 index 0000000..a84448a --- /dev/null +++ b/terraform/shared/cloudflare/main.tf @@ -0,0 +1,4 @@ +resource "cloudflare_r2_bucket" "state" { + account_id = var.cloudflare_account_id + name = var.r2_bucket_name +} diff --git a/terraform/shared/cloudflare/provider.tf b/terraform/shared/cloudflare/provider.tf new file mode 100644 index 0000000..b6d16e8 --- /dev/null +++ b/terraform/shared/cloudflare/provider.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.0" + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 5.0" + } + } +} + +provider "cloudflare" { + api_token = var.cloudflare_api_token +} diff --git a/terraform/shared/cloudflare/variables.tf b/terraform/shared/cloudflare/variables.tf new file mode 100644 index 0000000..3d7fd49 --- /dev/null +++ b/terraform/shared/cloudflare/variables.tf @@ -0,0 +1,16 @@ +variable "cloudflare_account_id" { + description = "Cloudflare Account ID (visible on Dashboard sidebar)" + type = string +} + +variable "cloudflare_api_token" { + description = "Cloudflare API Token with R2:Edit permission" + type = string + sensitive = true +} + +variable "r2_bucket_name" { + description = "R2 bucket name shared between Cloud Run and Compute Engine" + type = string + default = "openclaw-state" +} diff --git a/terraform/gcp_cloudrun/openclaw.json.tpl b/terraform/shared/openclaw.json.tpl similarity index 88% rename from terraform/gcp_cloudrun/openclaw.json.tpl rename to terraform/shared/openclaw.json.tpl index 3b38b29..140aaae 100644 --- a/terraform/gcp_cloudrun/openclaw.json.tpl +++ b/terraform/shared/openclaw.json.tpl @@ -47,10 +47,22 @@ "deny": ["browser", "apply_patch"] }, "plugins": { +%{ if use_plugin_load_paths ~} + "load": { + "paths": [ + "/usr/lib/node_modules/openclaw/dist/extensions/telegram"%{ if slack_enabled }, + "/usr/lib/node_modules/openclaw/dist/extensions/slack"%{ endif } + ] + }, +%{ endif ~} "entries": { +%{ if bonjour_enabled ~} "bonjour": { "enabled": false }, +%{ endif ~} "telegram": { "enabled": true }, +%{ if slack_enabled ~} "slack": { "enabled": true }, +%{ endif ~} "openrouter": { "enabled": true }, "brave": { "enabled": true, @@ -66,16 +78,19 @@ "channels": { "telegram": { "enabled": true, - "allowFrom": ["*"], "accounts": { "default": { "botToken": "${telegram_bot_token}", - "allowFrom": ["*"], +%{ if telegram_owner_id != "" ~} + "allowFrom": ["${telegram_owner_id}"], + "dmPolicy": "allowlist", +%{ else ~} "dmPolicy": "open", +%{ endif ~} "groupPolicy": "open" } } - }, + }%{ if slack_enabled }, "slack": { "enabled": true, "mode": "socket", @@ -84,6 +99,6 @@ "botToken": "${slack_bot_token}", "dmPolicy": "open", "groupPolicy": "open" - } + }%{ endif } } }