diff --git a/tools/caretaker-agent/cloudrun/pr-generator/Dockerfile b/tools/caretaker-agent/cloudrun/pr-generator/Dockerfile new file mode 100644 index 0000000000..00a6a5455b --- /dev/null +++ b/tools/caretaker-agent/cloudrun/pr-generator/Dockerfile @@ -0,0 +1,38 @@ +# Dockerfile for Jetski/Antigravity Worker Job using the Python SDK +# This container runs the manager script (workflow/worker.py) which orchestrates +# the code generation and evaluation in-process using google-antigravity and Firestore synchronization. + +FROM python:3.11-slim + +# 1. Install system utilities (git is required for repository operations) +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy Node.js 20 and npm directly from the official node:20-slim image +COPY --from=node:20-slim /usr/local/bin/node /usr/local/bin/node +COPY --from=node:20-slim /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx + +# 2. Create non-root system user and establish working directory +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# 3. Install Python dependencies using requirements.txt for optimized build layer caching +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + + +# 4. Copy the pipeline orchestration script and prompts with non-root ownership +COPY --chown=appuser:appuser workflow/ /app/workflow/ +COPY --chown=appuser:appuser agent_prompts/ /app/agent_prompts/ + +# 5. Switch to unprivileged user +USER appuser + +# 6. Execute script directly +ENTRYPOINT ["python", "/app/workflow/worker.py"] diff --git a/tools/caretaker-agent/cloudrun/pr-generator/job.yaml b/tools/caretaker-agent/cloudrun/pr-generator/job.yaml new file mode 100644 index 0000000000..51176ed69d --- /dev/null +++ b/tools/caretaker-agent/cloudrun/pr-generator/job.yaml @@ -0,0 +1,40 @@ +apiVersion: 'run.googleapis.com/v1' +kind: 'Job' +metadata: + labels: + cloud.googleapis.com/location: 'us-central1' + name: 'pr-gen-job' +spec: + template: + metadata: + annotations: + run.googleapis.com/client-name: 'gcloud' + run.googleapis.com/client-version: '575.0.1' + run.googleapis.com/execution-environment: 'gen2' + spec: + taskCount: 1 + template: + spec: + containers: + - env: + - name: 'GOOGLE_CLOUD_LOCATION' + value: 'global' + - name: 'MODEL_NAME' + value: 'gemini-3.5-flash' + - name: 'FIRESTORE_DATABASE' + value: 'gcli-db' + - name: 'FIRESTORE_COLLECTION' + value: 'issues' + - name: 'GIT_TOKEN' + valueFrom: + secretKeyRef: + key: 'latest' + name: 'PR_GEN_GITHUB_PUSH_KEY' + image: 'us-central1-docker.pkg.dev/gcli-intern-project-2026/pr-gen-repo/jetski-worker:latest' + resources: + limits: + cpu: '2' + memory: '8Gi' + maxRetries: 2 + serviceAccountName: 'code-gen-job-execution-sa@gcli-intern-project-2026.iam.gserviceaccount.com' + timeoutSeconds: '3600' diff --git a/tools/caretaker-agent/cloudrun/pr-generator/requirements.txt b/tools/caretaker-agent/cloudrun/pr-generator/requirements.txt new file mode 100644 index 0000000000..b10ebd014f --- /dev/null +++ b/tools/caretaker-agent/cloudrun/pr-generator/requirements.txt @@ -0,0 +1,6 @@ +google-antigravity>=0.1.0 +protobuf>=7.35.0 +pydantic +google-cloud-firestore>=2.15.0, <3.0.0 +google-cloud-storage>=2.14.0 +google-genai>=2.0.0 diff --git a/tools/caretaker-agent/cloudrun/pr-generator/workflow.yaml b/tools/caretaker-agent/cloudrun/pr-generator/workflow.yaml new file mode 100644 index 0000000000..c6a5eb0f27 --- /dev/null +++ b/tools/caretaker-agent/cloudrun/pr-generator/workflow.yaml @@ -0,0 +1,103 @@ +# Google Cloud Workflow that triggers Cloud Run Job and updates Firestore on failure. + +main: + params: ['event'] + steps: + - init: + assign: + - project_id: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}' + - database_id: '${default(sys.get_env("FIRESTORE_DATABASE"), "gcli-db")}' + - collection_name: '${default(sys.get_env("FIRESTORE_COLLECTION"), "issues")}' + - job_name: 'pr-gen-job' # The Cloud Run Job name + - job_location: 'us-central1' + - workflow_execution_id: '${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID")}' + + # Decode Pub/Sub message + - pubsub_message_bytes: '${base64.decode(event.data.message.data)}' + - firestore_doc_str: '${text.decode(pubsub_message_bytes)}' + - firestore_doc: '${json.decode(firestore_doc_str)}' + - doc_id: '${"github_" + firestore_doc.github_metadata.owner + "_" + firestore_doc.github_metadata.repo + "_" + string(firestore_doc.github_metadata.issue_number)}' + - repo_url: '${"https://github.com/" + firestore_doc.github_metadata.owner + "/" + firestore_doc.github_metadata.repo}' + - status: 'STARTED' + - error_details: null + + - validate_doc_id: + switch: + - condition: '${not text.match_regex(doc_id, "^[a-zA-Z0-9_.-]+$")}' + raise: '${"Security Exception: Invalid or unsafe firestore_id format: " + string(doc_id)}' + + - run_job: + try: + call: 'googleapis.run.v1.namespaces.jobs.run' + args: + name: '${"namespaces/" + project_id + "/jobs/" + job_name}' + location: '${job_location}' + body: + overrides: + containerOverrides: + - env: + - name: 'FIRESTORE_DOC' + value: '${firestore_doc_str}' + - name: 'REPO_URL' + value: '${repo_url}' + - name: 'USE_ADC' + value: 'true' + - name: 'EXECUTION_ID' + value: '${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID")}' + - name: 'GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID' + value: '${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID")}' + - name: 'FIRESTORE_ID' + value: '${doc_id}' + connector_params: + timeout: 7200 + result: 'job_result' + except: + as: 'e' + steps: + - handle_job_error: + assign: + - status: 'NEEDS_HUMAN' + - error_details: '${e}' + - update_firestore_on_failure: + try: + call: 'googleapis.firestore.v1.projects.databases.documents.patch' + args: + name: '${"projects/" + project_id + "/databases/" + database_id + "/documents/" + collection_name + "/" + doc_id}' + updateMask: + fieldPaths: + - 'status' + - 'error' + - 'lock.holder' + - 'lock.expires_at' + body: + fields: + status: + stringValue: 'NEEDS_HUMAN' + error: + stringValue: '${"Workflow Execution " + sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID") + " failed: " + json.encode_to_string(error_details)}' + lock: + mapValue: + fields: + holder: + nullValue: 'NULL_VALUE' + expires_at: + nullValue: 'NULL_VALUE' + except: + as: 'fs_err' + steps: + - log_firestore_error: + assign: + - firestore_error: '${fs_err}' + next: 'handle_failure' + next: 'mark_completed' + + - handle_failure: + return: '${"Failed: " + json.encode_to_string(error_details)}' + + - mark_completed: + assign: + - status: 'COMPLETED' + next: 'finish' + + - finish: + return: '${status}'