Skip to content

Conversation

@heidi-dang
Copy link

@heidi-dang heidi-dang commented Jan 27, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated Gemini API for assistant chat functionality
    • Added health and readiness check endpoints for monitoring
    • Automated one-click VPS deployment with Traefik SSL/TLS support
    • Full Docker containerization for local development and production
  • Chores

    • Enhanced CI/CD pipelines with automated testing and Docker image building
    • Added development roadmap and deployment documentation

✏️ Tip: You can customize this high-level summary in your review settings.

heidi-dang and others added 26 commits January 27, 2026 19:24
Stabilize workflows and guard .code in PR checks
Remove master from workflow filters
Guard against tracked .env and .code
Fix Traefik Docker API version mismatch
Automate VPS deploy via deploy.sh
@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces Docker containerization, a comprehensive CI/CD pipeline with GitHub Actions workflows, automated VPS deployment via shell scripts, Gemini API integration for assistant chat, health/readiness endpoints for monitoring, and supporting configuration and documentation updates.

Changes

Cohort / File(s) Change Summary
Docker & Container Configuration
Dockerfile, docker-compose.yml, docker-compose.traefik.yml
Multi-stage Dockerfile for UI build and Python runtime; docker-compose configuration for local Autocoder service with volume persistence and port mapping; Traefik reverse proxy setup with Let's Encrypt TLS, routing, and network configuration.
Deployment Automation
deploy.sh, scripts/deploy.sh
Two bash deployment scripts (root and scripts/ directory): one-click Docker deployment to VPS with DuckDNS, Traefik, Let's Encrypt integration; handles validation, configuration prompts, Docker installation, SSL preparation, health checks, and post-deployment verification.
GitHub Actions Workflows
.github/workflows/ci.yml, .github/workflows/deploy.yml, .github/workflows/pr-check.yml
Push CI workflow with repo-guards validation and Docker image build/push to GHCR; new Deploy to VPS workflow triggered after successful Push CI with SSH execution of deploy.sh and health checks; PR check workflow with repo-guards, Python linting/testing, and Node.js UI build/lint.
Configuration Files
.dockerignore, .gitignore, requirements.txt
New .dockerignore with standard ignore patterns; .gitignore additions for .code/ directory; requirements.txt addition of openai>=1.52.0 dependency.
Gemini API Integration
server/gemini_client.py, server/services/assistant_chat_session.py
New Gemini API client with OpenAI-compatible streaming chat; AssistantChatSession extended with provider selection, system prompt handling, and routing between Gemini and Claude backends.
Server API & Schema Updates
server/main.py, server/schemas.py
New health and readiness endpoints for monitoring; SetupStatus schema extended with gemini boolean field; setup status logic updated to track Gemini API configuration.
UI Updates
ui/src/components/SetupWizard.tsx, ui/src/lib/types.ts
SetupWizard component updated with optional Gemini setup item; SetupStatus TypeScript interface extended with gemini boolean field.
Documentation & Tests
README.md, DEVELOPMENT.md, test_health.py
README with Gemini API configuration details, CI/CD workflow descriptions, branch protection setup; new DEVELOPMENT.md roadmap outlining phased improvements; test_health.py with lightweight health/readiness endpoint tests.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant App as AssistantChatSession
    participant GeminiClient as gemini_client
    participant GeminiAPI as Gemini API<br/>(OpenAI-compatible)

    User->>App: send_message("user query")
    rect rgba(100, 150, 200, 0.5)
    Note over App: Provider check: Gemini configured?
    end
    App->>GeminiClient: stream_chat(message, system_prompt)
    
    rect rgba(100, 150, 200, 0.5)
    Note over GeminiClient: Build AsyncOpenAI client<br/>with API key & base URL
    end
    
    GeminiClient->>GeminiAPI: POST /v1beta/openai/chat/completions (stream=true)
    GeminiAPI-->>GeminiClient: Streaming completion chunks
    
    rect rgba(100, 150, 200, 0.5)
    Note over GeminiClient: Iterate chunks → extract delta.content
    end
    
    GeminiClient-->>App: Yield text tokens (async generator)
    App->>App: Accumulate full response
    App-->>User: Stream response tokens
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit's ode to deployment dreams:

From Docker's cozy Alpine streams,
Through Traefik's TLS schemes,
Gemini chats with gentle beams,
CI/CD fulfills our means—
Health checks hop through every seam! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title is vague and overly broad. It mentions 'traefik routing' and 'remote' but the changeset introduces comprehensive CI/CD pipelines, Docker containerization, Gemini integration, deployment automation, and multiple infrastructure files. The title does not reflect the main scope of changes. Consider a more descriptive title that captures the primary objective, such as 'Add CI/CD pipelines, Docker deployment, and Gemini integration' or split into multiple focused PRs.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @.github/workflows/pr-check.yml:
- Around line 3-5: The workflow trigger is configured to use the non-existent
branch name "main" (see the on: pull_request: branches: [main] block); update
this across the three workflow files (pr-check.yml, deploy.yml, ci.yml) by
changing branches: [main] to branches: [master] so PR events target the
repository's default branch; ensure you update the same "on: pull_request:"
block in each file and commit the three modified workflow files.

In `@deploy.sh`:
- Around line 165-173: The cron file written by configure_duckdns contains the
DUCKDNS_TOKEN and is created world-readable (0644); change the file creation and
permissions so only root can read it: after writing the cron_file in
configure_duckdns, set owner to root:root and restrict mode to 0600 (or use
install/umask to create it with 0600) to prevent other local users from reading
DUCKDNS_TOKEN; reference symbols: configure_duckdns, cron_file, DUCKDNS_TOKEN.

In `@scripts/deploy.sh`:
- Around line 74-83: The DuckDNS update URL in configure_duckdns uses $DOMAIN
which may be a full hostname but DuckDNS expects only the subdomain; update
configure_duckdns to extract the subdomain before building the URL (e.g., strip
suffix like ".duckdns.org" from DOMAIN) and use that extracted variable in both
the cron entry and the immediate curl, referencing the configure_duckdns
function and variables DOMAIN and DUCKDNS_TOKEN; alternatively, update the user
prompt where DOMAIN is set to require just the subdomain and ensure any Traefik
logic derives the full domain from that subdomain consistently.
- Around line 13-25: The export in prompt_required() uses export "$var_name"
which exports the literal string instead of the named variable; remove the
ineffective export or change it to export "${var_name?}" (or use declare -x
"$var_name") after printf -v sets the variable so the actual variable named by
var_name is exported; update prompt_required to either omit the export (since
printf -v already sets the variable) or replace export "$var_name" with export
"${var_name?}" / declare -x "$var_name" to correctly export the named variable.

In `@server/gemini_client.py`:
- Around line 37-80: The streaming handler in stream_chat incorrectly assumes
delta.content is a list and iterates strings as sequences; update the delta
handling in the async for chunk loop (inside stream_chat) to detect the type of
delta.content and handle each correctly: if delta.content is a str yield it as a
single chunk; if it is a list iterate parts and extract part.text or
part.get("text") when a dict (or use getattr for objects); if it's a dict
extract its "text" similarly; ensure you avoid iterating over raw strings so you
don't yield single characters.

In `@server/main.py`:
- Around line 119-136: There are two inconsistent health endpoints—health()
returning {"status": "ok"} and the /api/health handler returning {"status":
"healthy"}—so reconcile them: decide whether /health is an infra liveness probe
and /api/health is an application-level readiness check or consolidate both to
the same semantic and payload; then update the handlers (health(), readiness(),
and the /api/health handler) to return a consistent status string/shape and add
a brief docstring to each explaining their distinct purpose (or merge routes so
only one handler serves both paths) so consumers receive a single,
well-documented contract.
🧹 Nitpick comments (12)
.gitignore (1)

9-23: Duplicate .code/ entry in .gitignore.

The .code/ directory is listed twice: at line 10 under "Repository-specific" and at line 23 under "Local Codex/Claude configuration". Consider consolidating into a single entry to avoid confusion.

Suggested consolidation
-# Repository-specific
-.code/
-
 # Browser profiles for parallel agent execution
 .browser-profiles/

Keep only the more descriptive entry at lines 22-23.

Dockerfile (1)

12-25: Consider running the container as a non-root user for improved security.

The runtime stage runs as root by default. For production deployments, consider adding a non-root user to reduce the attack surface if the container is compromised.

Suggested non-root user setup
 FROM python:3.11-slim AS runtime
 ENV PYTHONUNBUFFERED=1 \
     PYTHONDONTWRITEBYTECODE=1

 WORKDIR /app
+
+# Create non-root user
+RUN useradd --create-home --shell /bin/bash appuser
+
 COPY requirements.txt .
 RUN pip install --no-cache-dir -r requirements.txt

 # Copy source code and built UI
 COPY . .
 COPY --from=ui-builder /app/ui/dist ./ui/dist

+# Change ownership and switch to non-root user
+RUN chown -R appuser:appuser /app
+USER appuser
+
 EXPOSE 8888
 CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8888"]

Note: If the application requires writing to /root/.autocoder (as indicated by the volume mount in docker-compose.yml), you'll need to adjust the volume path accordingly.

docker-compose.yml (2)

1-1: The version key is obsolete in Docker Compose.

The version field is deprecated in the Compose Specification and is ignored by recent versions of Docker Compose. While harmless, it can be removed for cleanliness.

Remove deprecated version key
-version: "3.9"
-
 services:

21-21: Redundant command override.

The command duplicates the CMD in the Dockerfile. This is harmless but can be removed unless you need the flexibility to override it differently in other compose files.

.github/workflows/pr-check.yml (1)

38-52: Consider adding type checking to the Python job.

The workflow runs ruff check for linting but the project includes mypy>=1.13.0 in dev dependencies. Consider adding mypy for static type checking.

Add mypy type checking
       - name: Lint with ruff
         run: ruff check .
+      - name: Type check with mypy
+        run: mypy server/
       - name: Run security tests
         run: python test_security.py
requirements.txt (1)

13-13: Version 1.52.0 is valid; upper bound suggestion is overly restrictive.

The openai>=1.52.0 dependency is confirmed to exist and has no known security vulnerabilities. However, the suggested upper bound <2.0.0 is unnecessarily restrictive since version 2.15.0 is the current stable release. If version constraints are desired for consistency with claude-agent-sdk<0.2.0 and apscheduler<4.0.0, use openai>=1.52.0,<3.0.0 instead, or verify that version 2.x is compatible before removing the upper bound entirely.

docker-compose.traefik.yml (2)

1-1: Consider removing the deprecated version field.

Docker Compose V2 ignores the version field. Removing it avoids confusion and aligns with current best practices.

♻️ Suggested change
-version: "3.9"
-
 services:

5-11: Consider pinning the Traefik version and making DOCKER_API_VERSION configurable.

Using traefik:latest may introduce breaking changes unexpectedly. Pinning to a specific version (e.g., traefik:v3.2) improves reproducibility.

The hardcoded DOCKER_API_VERSION=1.53 may cause issues on systems with older Docker daemons. Consider using a variable with a sensible default.

♻️ Suggested change
   traefik:
-    image: traefik:latest
+    image: traefik:v3.2
     environment:
       # Force a modern Docker API version. Some VPS environments set
       # DOCKER_API_VERSION=1.24 globally, which breaks Traefik's Docker provider
       # when the daemon requires >= 1.44.
       # Use the server's current API version to avoid a too-old client default.
-      - DOCKER_API_VERSION=1.53
+      - DOCKER_API_VERSION=${DOCKER_API_VERSION:-1.53}
.github/workflows/ci.yml (1)

40-43: Consider running the new health endpoint tests in CI.

The python job runs test_security.py but not test_health.py. Consider adding pytest or running the new health tests to ensure endpoint regressions are caught.

♻️ Suggested addition
       - name: Run security tests
         run: python test_security.py
+      - name: Run health tests
+        run: python -m pytest test_health.py -v

This would require adding pytest to requirements.txt if not already present.

deploy.sh (1)

92-141: Consider defaulting DEPLOY_BRANCH to the repo’s default branch.
If the repo doesn’t have a main branch, the default path fails. Consider deriving from remote HEAD after clone or aligning the default with the repo’s primary branch.

.github/workflows/deploy.yml (2)

33-40: Consider pinning the action to a commit SHA for enhanced supply chain security.

While v1.2.4 is a pinned version, tags can be moved. For critical deployment workflows, pinning to a specific commit SHA provides stronger guarantees against supply chain attacks.

🔒 Suggested improvement
-        uses: appleboy/ssh-action@v1.2.4
+        uses: appleboy/ssh-action@<full-commit-sha>  # v1.2.4

You can find the commit SHA for v1.2.4 on the action's releases page.


90-113: Health check implementation is solid.

The retry logic with 12 attempts and 5-second intervals provides a reasonable 60-second window for each endpoint to become available. The curl flags (-fsS --max-time 5) are appropriate for health checks.

Consider extracting the retry logic into a shell function to reduce duplication:

♻️ Optional: Extract retry logic into a function
+            wait_for_endpoint() {
+              local endpoint=$1
+              local retries=12
+              until curl -fsS --max-time 5 "http://127.0.0.1:${APP_PORT}${endpoint}" >/dev/null; do
+                retries=$((retries - 1))
+                if [ "$retries" -le 0 ]; then
+                  echo "${endpoint} check failed after retries."
+                  exit 1
+                fi
+                echo "Waiting for ${endpoint}... ($retries retries left)"
+                sleep 5
+              done
+            }
+
             echo "Running smoke test on http://127.0.0.1:${APP_PORT}/health and /readiness ..."
-            retries=12
-            until curl -fsS --max-time 5 "http://127.0.0.1:${APP_PORT}/health" >/dev/null; do
-              retries=$((retries - 1))
-              if [ "$retries" -le 0 ]; then
-                echo "Health check failed after retries."
-                exit 1
-              fi
-              echo "Waiting for health... ($retries retries left)"
-              sleep 5
-            done
-
-            retries=12
-            until curl -fsS --max-time 5 "http://127.0.0.1:${APP_PORT}/readiness" >/dev/null; do
-              retries=$((retries - 1))
-              if [ "$retries" -le 0 ]; then
-                echo "Readiness check failed after retries."
-                exit 1
-              fi
-              echo "Waiting for readiness... ($retries retries left)"
-              sleep 5
-            done
+            wait_for_endpoint "/health"
+            wait_for_endpoint "/readiness"

Comment on lines +3 to +5
on:
pull_request:
branches: [main]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the default branch and workflow branch triggers

echo "=== Default branch ==="
git remote show origin 2>/dev/null | grep 'HEAD branch' || git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "Could not determine default branch"

echo ""
echo "=== Branches in the repository ==="
git branch -r | head -20

echo ""
echo "=== Branch triggers in all workflows ==="
rg -n "branches:" .github/workflows/ --type yaml || echo "No branch triggers found"

echo ""
echo "=== Content of pr-check.yml (first 20 lines) ==="
head -20 .github/workflows/pr-check.yml

Repository: leonvanzyl/autocoder

Length of output: 844


Critical: All workflows configured to trigger on non-existent main branch, but repository uses master.

The repository's default branch is master, but all three workflows (pr-check.yml, deploy.yml, ci.yml) are configured to trigger on branches: [main], which doesn't exist. These workflows won't run for any pull requests.

Update all workflow files to trigger on master:

Fix all workflows
 on:
   pull_request:
-    branches: [main]
+    branches: [master]

Apply this change to .github/workflows/pr-check.yml, .github/workflows/deploy.yml, and .github/workflows/ci.yml.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
pull_request:
branches: [main]
on:
pull_request:
branches: [master]
🤖 Prompt for AI Agents
In @.github/workflows/pr-check.yml around lines 3 - 5, The workflow trigger is
configured to use the non-existent branch name "main" (see the on: pull_request:
branches: [main] block); update this across the three workflow files
(pr-check.yml, deploy.yml, ci.yml) by changing branches: [main] to branches:
[master] so PR events target the repository's default branch; ensure you update
the same "on: pull_request:" block in each file and commit the three modified
workflow files.

Comment on lines +165 to +173
configure_duckdns() {
echo
echo "==> Configuring DuckDNS..."
local cron_file="/etc/cron.d/duckdns"
cat > "${cron_file}" <<EOF
*/5 * * * * root curl -fsS "https://www.duckdns.org/update?domains=${DUCKDNS_SUBDOMAIN}&token=${DUCKDNS_TOKEN}&ip=" >/var/log/duckdns.log 2>&1
EOF
chmod 644 "${cron_file}"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Protect DuckDNS token from world-readable cron file.
The token is written to a 0644 cron file, making it readable by any local user. Restrict it to root to avoid leakage.

🔒 Proposed fix
-  chmod 644 "${cron_file}"
+  chmod 600 "${cron_file}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
configure_duckdns() {
echo
echo "==> Configuring DuckDNS..."
local cron_file="/etc/cron.d/duckdns"
cat > "${cron_file}" <<EOF
*/5 * * * * root curl -fsS "https://www.duckdns.org/update?domains=${DUCKDNS_SUBDOMAIN}&token=${DUCKDNS_TOKEN}&ip=" >/var/log/duckdns.log 2>&1
EOF
chmod 644 "${cron_file}"
configure_duckdns() {
echo
echo "==> Configuring DuckDNS..."
local cron_file="/etc/cron.d/duckdns"
cat > "${cron_file}" <<EOF
*/5 * * * * root curl -fsS "https://www.duckdns.org/update?domains=${DUCKDNS_SUBDOMAIN}&token=${DUCKDNS_TOKEN}&ip=" >/var/log/duckdns.log 2>&1
EOF
chmod 600 "${cron_file}"
}
🤖 Prompt for AI Agents
In `@deploy.sh` around lines 165 - 173, The cron file written by configure_duckdns
contains the DUCKDNS_TOKEN and is created world-readable (0644); change the file
creation and permissions so only root can read it: after writing the cron_file
in configure_duckdns, set owner to root:root and restrict mode to 0600 (or use
install/umask to create it with 0600) to prevent other local users from reading
DUCKDNS_TOKEN; reference symbols: configure_duckdns, cron_file, DUCKDNS_TOKEN.

Comment on lines +13 to +25
prompt_required() {
local var_name="$1" prompt_msg="$2"
local value
while true; do
read -r -p "$prompt_msg: " value
if [[ -n "$value" ]]; then
printf -v "$var_name" '%s' "$value"
export "$var_name"
return
fi
echo "Value cannot be empty."
done
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the ineffective export statement (SC2163).

The export "$var_name" on line 20 exports the literal string value of $var_name, not the variable it names. The printf -v already sets the variable correctly, but if you need it exported, use export "${var_name?}" or declare it beforehand.

🔧 Suggested fix
 prompt_required() {
   local var_name="$1" prompt_msg="$2"
   local value
   while true; do
     read -r -p "$prompt_msg: " value
     if [[ -n "$value" ]]; then
       printf -v "$var_name" '%s' "$value"
-      export "$var_name"
+      export "${var_name?}"
       return
     fi
     echo "Value cannot be empty."
   done
 }
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 20-20: This does not export 'var_name'. Remove $/${} for that, or use ${var?} to quiet.

(SC2163)

🤖 Prompt for AI Agents
In `@scripts/deploy.sh` around lines 13 - 25, The export in prompt_required() uses
export "$var_name" which exports the literal string instead of the named
variable; remove the ineffective export or change it to export "${var_name?}"
(or use declare -x "$var_name") after printf -v sets the variable so the actual
variable named by var_name is exported; update prompt_required to either omit
the export (since printf -v already sets the variable) or replace export
"$var_name" with export "${var_name?}" / declare -x "$var_name" to correctly
export the named variable.

Comment on lines +74 to +83
configure_duckdns() {
echo "Configuring DuckDNS..."
local cron_file="/etc/cron.d/duckdns"
cat > "$cron_file" <<EOF
*/5 * * * * root curl -fsS "https://www.duckdns.org/update?domains=$DOMAIN&token=$DUCKDNS_TOKEN&ip=" >/var/log/duckdns.log 2>&1
EOF
chmod 644 "$cron_file"
# Run once immediately
curl -fsS "https://www.duckdns.org/update?domains=$DOMAIN&token=$DUCKDNS_TOKEN&ip=" >/var/log/duckdns.log 2>&1 || true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

DuckDNS API expects the subdomain only, not the full domain.

The DuckDNS update URL on line 78 uses $DOMAIN directly, but the API expects just the subdomain (e.g., myapp), not the full domain (e.g., myapp.duckdns.org). If users enter the full domain as prompted, the update will fail.

🔧 Suggested fix - extract subdomain
 configure_duckdns() {
   echo "Configuring DuckDNS..."
+  # Extract subdomain from full domain (myapp.duckdns.org -> myapp)
+  local subdomain="${DOMAIN%%.*}"
   local cron_file="/etc/cron.d/duckdns"
   cat > "$cron_file" <<EOF
-*/5 * * * * root curl -fsS "https://www.duckdns.org/update?domains=$DOMAIN&token=$DUCKDNS_TOKEN&ip=" >/var/log/duckdns.log 2>&1
+*/5 * * * * root curl -fsS "https://www.duckdns.org/update?domains=$subdomain&token=$DUCKDNS_TOKEN&ip=" >/var/log/duckdns.log 2>&1
 EOF
   chmod 644 "$cron_file"
   # Run once immediately
-  curl -fsS "https://www.duckdns.org/update?domains=$DOMAIN&token=$DUCKDNS_TOKEN&ip=" >/var/log/duckdns.log 2>&1 || true
+  curl -fsS "https://www.duckdns.org/update?domains=$subdomain&token=$DUCKDNS_TOKEN&ip=" >/var/log/duckdns.log 2>&1 || true
 }

Alternatively, update the prompt on line 29 to clarify that users should enter the subdomain only, and derive the full domain for Traefik.

🤖 Prompt for AI Agents
In `@scripts/deploy.sh` around lines 74 - 83, The DuckDNS update URL in
configure_duckdns uses $DOMAIN which may be a full hostname but DuckDNS expects
only the subdomain; update configure_duckdns to extract the subdomain before
building the URL (e.g., strip suffix like ".duckdns.org" from DOMAIN) and use
that extracted variable in both the cron entry and the immediate curl,
referencing the configure_duckdns function and variables DOMAIN and
DUCKDNS_TOKEN; alternatively, update the user prompt where DOMAIN is set to
require just the subdomain and ensure any Traefik logic derives the full domain
from that subdomain consistently.

Comment on lines +37 to +80
async def stream_chat(
user_message: str,
*,
system_prompt: Optional[str] = None,
model: Optional[str] = None,
extra_messages: Optional[Iterable[dict]] = None,
) -> AsyncGenerator[str, None]:
"""
Stream a chat completion from Gemini.

Args:
user_message: Primary user input
system_prompt: Optional system prompt to prepend
model: Optional model name; defaults to GEMINI_MODEL env or fallback constant
extra_messages: Optional prior messages (list of {"role","content"})
Yields:
Text chunks as they arrive.
"""
client = _build_client()
messages = []

if system_prompt:
messages.append({"role": "system", "content": system_prompt})

if extra_messages:
messages.extend(extra_messages)

messages.append({"role": "user", "content": user_message})

completion = await client.chat.completions.create(
model=model or DEFAULT_GEMINI_MODEL,
messages=messages,
stream=True,
)

async for chunk in completion:
for choice in chunk.choices:
delta = choice.delta
if delta and delta.content:
# delta.content is a list of content parts
for part in delta.content:
text = getattr(part, "text", None) or part.get("text") if isinstance(part, dict) else None
if text:
yield text
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the actual file to understand imports and context
head -40 server/gemini_client.py

Repository: leonvanzyl/autocoder

Length of output: 1243


🏁 Script executed:

# Check for any requirements or setup files that indicate SDK versions
find . -name "requirements*.txt" -o -name "pyproject.toml" -o -name "setup.py" | head -5

Repository: leonvanzyl/autocoder

Length of output: 99


🏁 Script executed:

# Search for how delta.content is used elsewhere in the codebase
rg "delta\.content" --type py -B 2 -A 2

Repository: leonvanzyl/autocoder

Length of output: 583


🌐 Web query:

OpenAI Python SDK 1.52+ chat completions streaming delta.content format

💡 Result:

In openai-python SDK 1.52+, client.chat.completions.create(..., stream=True) yields ChatCompletionChunk objects. Each chunk has:

  • chunk.choices[0].delta (a “delta” object), which may include:
    • delta.role (typically appears once at the start)
    • delta.content (string text increment, sometimes ""/None on some chunks)
    • delta.tool_calls (when the model is calling tools; in these chunks delta.content is often absent)
    • delta.refusal (if refusing)
      These fields are documented as part of the streamed “chat completion chunk” schema. [1]

Typical usage (text streaming):

stream = client.chat.completions.create(..., stream=True)

for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="")

This exact chunk.choices[0].delta.content access pattern is also shown in OpenAI’s streaming example. [2]

Notes you’ll see in real streams:

  • The first chunk often includes delta.role="assistant" and delta.content="". [1]
  • If you set stream_options={"include_usage": true}, the last chunk may have choices=[] (empty). [1]

If you meant the newer “Responses API” streaming: it does not use delta.content; you’ll instead receive events like response.output_text.delta with a delta string payload. [3]

Sources: [1] Chat Completions streaming chunk schema, [2] OpenAI Cookbook streaming example, [3] Responses streaming guide.


🌐 Web query:

Google Gemini OpenAI-compatible API endpoint delta.content structure

💡 Result:

Endpoint (Gemini API “OpenAI compatibility”)

Use the OpenAI Chat Completions path under the /openai/ prefix:

  • Base URL: https://generativelanguage.googleapis.com/v1beta/openai/
  • Chat Completions: POST https://generativelanguage.googleapis.com/v1beta/openai/chat/completions (ai.google.dev)

Streaming delta.content structure

In streaming mode ("stream": true / stream=True), Gemini returns OpenAI-style chat completion chunks where each SSE event contains (conceptually):

{
  "choices": [
    {
      "delta": {
        "content": "…partial text…"
      }
    }
  ]
}
  • choices[].delta.content is a string containing the next incremental piece of the assistant message (and can be omitted/null on chunks that carry other fields). (platform.openai.com)
  • The docs’ streaming examples access it exactly like OpenAI: chunk.choices[0].delta / chunk.choices[0].delta.content. (ai.google.dev)

Known compatibility quirks (worth handling defensively)

  • role may be repeated in later streamed delta chunks (not typical in OpenAI streaming), per a reported/acknowledged issue. (discuss.ai.google.dev)
  • For tool calling + streaming, Gemini has been reported to emit delta.tool_calls[] entries without the inner tool_call.index field (some OpenAI parsers expect it). (discuss.ai.google.dev)

Citations:


Handle string delta.content in streaming responses.

OpenAI Python SDK 1.52+ and Gemini's OpenAI-compatible endpoint return delta.content as a string, not a list. The current code iterates through it as a list, yielding individual characters instead of proper text chunks—a major inefficiency that defeats the purpose of streaming.

Proposed fix
     async for chunk in completion:
         for choice in chunk.choices:
             delta = choice.delta
             if delta and delta.content:
-                # delta.content is a list of content parts
-                for part in delta.content:
-                    text = getattr(part, "text", None) or part.get("text") if isinstance(part, dict) else None
-                    if text:
-                        yield text
+                if isinstance(delta.content, str):
+                    yield delta.content
+                else:
+                    # Fallback: handle list case for other endpoints
+                    for part in delta.content:
+                        text = getattr(part, "text", None) or (part.get("text") if isinstance(part, dict) else None)
+                        if text:
+                            yield text
🤖 Prompt for AI Agents
In `@server/gemini_client.py` around lines 37 - 80, The streaming handler in
stream_chat incorrectly assumes delta.content is a list and iterates strings as
sequences; update the delta handling in the async for chunk loop (inside
stream_chat) to detect the type of delta.content and handle each correctly: if
delta.content is a str yield it as a single chunk; if it is a list iterate parts
and extract part.text or part.get("text") when a dict (or use getattr for
objects); if it's a dict extract its "text" similarly; ensure you avoid
iterating over raw strings so you don't yield single characters.

Comment on lines +119 to +136
# ============================================================================
# Health Endpoint
# ============================================================================

@app.get("/health")
async def health():
"""Lightweight liveness probe used by deploy smoke tests."""
return {"status": "ok"}


@app.get("/readiness")
async def readiness():
"""
Readiness probe placeholder.

Add dependency checks (DB, external APIs, queues) here when introduced.
"""
return {"status": "ready"}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the relationship between /health and /api/health endpoints.

There are now two health endpoints:

  • /health (Line 123) returns {"status": "ok"}
  • /api/health (Line 187) returns {"status": "healthy"}

This inconsistency may confuse consumers. Consider consolidating them or documenting their distinct purposes (e.g., /health for infrastructure probes vs /api/health for application-level checks).

🤖 Prompt for AI Agents
In `@server/main.py` around lines 119 - 136, There are two inconsistent health
endpoints—health() returning {"status": "ok"} and the /api/health handler
returning {"status": "healthy"}—so reconcile them: decide whether /health is an
infra liveness probe and /api/health is an application-level readiness check or
consolidate both to the same semantic and payload; then update the handlers
(health(), readiness(), and the /api/health handler) to return a consistent
status string/shape and add a brief docstring to each explaining their distinct
purpose (or merge routes so only one handler serves both paths) so consumers
receive a single, well-documented contract.

getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 28, 2026
getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 28, 2026
- Fix workflows to trigger on master branch instead of main
- Protect DuckDNS cron file with chmod 600 (security fix)
- Fix gemini_client.py to handle string delta.content in streaming
- Fix scripts/deploy.sh to extract DuckDNS subdomain properly
rudiheydra added a commit to rudiheydra/AutoBuildr that referenced this pull request Jan 28, 2026
Create test_persistence_after_kernel_run() proving that after one
HarnessKernel run, the DB contains AgentSpec, AgentRun, and AgentEvent
records with correct foreign keys and ascending event sequences.

- Creates AgentSpec and persists to in-memory SQLite
- Executes via HarnessKernel with mocked turn_executor (2 turns)
- Queries and verifies AgentSpec, AgentRun (FK), AgentEvent (FK + ordering)
- All 43 tests in test_dspy_pipeline_e2e.py pass
- Marked feature leonvanzyl#118 as passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leonvanzyl
Copy link
Owner

PR Review: Docker Deployment & Gemini Integration

Hey @heidi-dang! 👋 Thanks for this comprehensive PR - there's a lot of valuable work here including Docker containerization, CI/CD pipelines, VPS deployment automation, and Gemini API integration.

Given the scope (1,178 additions across 20 files), I've done a thorough review and found a few issues that should be addressed before merging.


🚨 Critical Issues (Must Fix)

1. Gemini Streaming Bug (server/gemini_client.py:72-80)

The code treats delta.content as a list of content parts, but the OpenAI SDK returns it as a plain string:

# Current (broken)
for part in delta.content:
    text = getattr(part, "text", None) or ...

# Should be
if delta and delta.content:
    yield delta.content  # It's already a string

This will cause character-by-character output or a TypeError at runtime.

2. CI Branch Mismatch (.github/workflows/*.yml)

The workflows target branches: [main] but the repository's default branch is master. This means:

  • PRs to master won't trigger PR Check
  • Pushes to master won't build Docker images
  • Deploy workflow won't trigger

Fix: Change to branches: [main, master] in all workflow files.

3. DuckDNS Token Exposure (deploy.sh:651-654)

The cron file is created with 644 permissions (world-readable), exposing the DuckDNS token to any user on the system:

chmod 644 "${cron_file}"  # Should be 600

⚠️ Security Recommendations

Docker Socket Mount (docker-compose.traefik.yml)

Mounting /var/run/docker.sock to the Traefik container enables potential container escape. Consider using a Docker socket proxy for better isolation.

Container Runs as Root (Dockerfile)

Adding a non-root user would improve security:

RUN useradd -m -u 1000 appuser
USER appuser

📝 Other Suggestions

Issue Suggestion
Duplicate health endpoints /health returns "ok", /api/health returns "healthy" - consider consolidating
Duplicate deploy scripts deploy.sh (371 lines) vs scripts/deploy.sh (133 lines) - consider removing the simpler one
No Gemini tests test_health.py only has 2 tests - consider adding coverage for the new Gemini client
TypeScript breaking change gemini: boolean should be optional (gemini?: boolean) for backwards compatibility
Hardcoded repo URL deploy.sh defaults to heidi-dang/autocoder.git instead of the canonical repo
Pin Traefik version Using traefik:latest may cause unexpected updates - consider pinning (e.g., traefik:v3.0)

Summary

This is a substantial infrastructure improvement, but the critical issues above need to be addressed first. The streaming bug in particular will cause the Gemini chat feature to malfunction.

Happy to re-review once the critical items are fixed! Let me know if you have any questions about the feedback. 🙂

getworken pushed a commit to getworken/autocoder that referenced this pull request Jan 29, 2026
- Fix workflows to trigger on master branch instead of main
- Protect DuckDNS cron file with chmod 600 (security fix)
- Fix gemini_client.py to handle string delta.content in streaming
- Fix scripts/deploy.sh to extract DuckDNS subdomain properly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants