diff --git a/current_state.txt b/current_state.txt index 6db924f..d150a2f 100644 --- a/current_state.txt +++ b/current_state.txt @@ -1,634 +1,3 @@ -. -├── backend -│ ├── Dockerfile -│ ├── .env -│ ├── __init__.py -│ ├── main.py -│ ├── pyproject.toml -│ └── src -│ ├── client.py -│ ├── functions -│ ├── __init__.py -│ ├── prompts.py -│ ├── services.py -│ └── workflows -├── docker-compose.yml -├── frontend -├── llm-output - -# ./docker-compose.yml -services: - restack-engine: - image: ghcr.io/restackio/restack:main - container_name: restack - restart: always - networks: - - restack-network - ports: - - "5233:5233" - - "6233:6233" - - "7233:7233" - - docker-dind: - image: docker:24-dind - privileged: true - command: ["dockerd", "--host=tcp://0.0.0.0:2375", "--tls=false"] - networks: - - restack-network - volumes: - - ./llm-output:/app/output:rw - - backend: - build: ./backend - environment: - - OPENAI_KEY=${OPENAI_KEY} - - DOCKER_HOST=tcp://docker-dind:2375 - - RESTACK_ENGINE_ADDRESS=http://restack:6233 - - RESTACK_TEMPORAL_ADDRESS=http://restack:7233 - - RESTACK_ENGINE_ID = "local" - - RESTACK_ENGINE_API_KEY = None - - LLM_OUTPUT_DIR=/app/output - depends_on: - - restack-engine - - docker-dind - command: ["poetry", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] - networks: - - restack-network - volumes: - - ./llm-output:/app/output:rw - ports: - - "8000:8000" - - worker: - build: ./backend - environment: - - OPENAI_KEY=${OPENAI_KEY} - - DOCKER_HOST=tcp://docker-dind:2375 - - RESTACK_ENGINE_ADDRESS=http://restack:6233 - - RESTACK_TEMPORAL_ADDRESS=http://restack:7233 - - RESTACK_ENGINE_ID = "local" - - RESTACK_ENGINE_API_KEY = None - - LLM_OUTPUT_DIR=/app/output - depends_on: - - restack-engine - - docker-dind - - backend - command: ["sh", "-c", "sleep 5 && poetry run python -m src.services"] - networks: - - restack-network - volumes: - - ./llm-output:/app/output:rw - - frontend: - build: ./frontend - depends_on: - - backend - command: ["npm", "run", "dev"] - networks: - - restack-network - ports: - - "8080:8080" - -networks: - restack-network: - driver: bridge - -#./backend/Dockerfile -FROM python:3.10-slim - -# Set working directory -WORKDIR /app - -# Copy Poetry configuration files -COPY pyproject.toml poetry.lock* /app/ - -# Install dependencies and Docker CLI tools -RUN apt-get update && apt-get install -y \ - ca-certificates \ - curl \ - gnupg \ - && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list \ - && apt-get update \ - && apt-get install -y docker-ce-cli docker-buildx-plugin docker-compose-plugin \ - && rm -rf /var/lib/apt/lists/* - -# Install Poetry -RUN pip install poetry && poetry install --no-root - -# Create and ensure permissions for output directory -RUN mkdir -p /app/output && chmod 777 /app/output - -# Copy application files -COPY . /app - -# Expose application port -EXPOSE 8000 - -#./backend/pyproject.toml -# Project metadata -[tool.poetry] -name = "azlon" -version = "0.0.1" -description = "autonomous coding project solver" -authors = [ - "Harrison E. Muchnic ", -] -readme = "readme.md" -packages = [{include = "src"}] - -[tool.poetry.dependencies] -python = ">=3.10,<4.0" -restack-ai = "0.0.50" -openai = "1.57.1" -pydantic = "^2.10.3" -fastapi = "0.115.4" -uvicorn = "^0.22.0" - -[tool.poetry.dev-dependencies] -pytest = "6.2" # Optional: Add if you want to include tests in your example - -# Build system configuration -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - -# CLI command configuration -[tool.poetry.scripts] -services = "src.services:run_services" -schedule = "schedule_workflow:run_schedule_workflow" - -# ./backend/main.py -from fastapi import FastAPI, HTTPException, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from pydantic import BaseModel -import time -import os - -from src.prompts import get_prompts, set_prompts -from restack_ai import Restack -from restack_ai.restack import CloudConnectionOptions - -RESTACK_ENGINE_ADDRESS = os.getenv('RESTACK_ENGINE_ADDRESS') -RESTACK_TEMPORAL_ADDRESS = os.getenv('RESTACK_TEMPORAL_ADDRESS') -RESTACK_ENGINE_ID = os.getenv('RESTACK_ENGINE_ID') -RESTACK_ENGINE_API_KEY = os.getenv('RESTACK_ENGINE_API_KEY') - -app = FastAPI() - -# CORS configuration -app.add_middleware( - CORSMiddleware, - allow_origins=["http://localhost:3000","http://localhost:8080"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"] -) - -class UserInput(BaseModel): - user_prompt: str - test_conditions: str - -class PromptsInput(BaseModel): - generate_code_prompt: str - validate_output_prompt: str - -@app.get("/prompts") -def fetch_prompts(): - return get_prompts() - -@app.post("/prompts") -def update_prompts(prompts: PromptsInput): - set_prompts(prompts.generate_code_prompt, prompts.validate_output_prompt) - return {"status": "updated"} - -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - return JSONResponse( - status_code=500, - content={"error": "Internal Server Error."}, - headers={"Access-Control-Allow-Origin": "http://localhost:8080"}, - ) - -@app.post("/run_workflow") -async def run_workflow(params: UserInput): - connection_options = CloudConnectionOptions( - engine_id=RESTACK_ENGINE_ID, - api_key=RESTACK_ENGINE_API_KEY, - address=RESTACK_TEMPORAL_ADDRESS, - api_address=RESTACK_ENGINE_ADDRESS, - temporal_namespace="default") - - # Initialize Restack with these options options=connection_options - client = Restack(connection_options) - try: - workflow_id = f"{int(time.time() * 1000)}-AutonomousCodingWorkflow" - runId = await client.schedule_workflow( - workflow_name="AutonomousCodingWorkflow", - workflow_id=workflow_id, - input=params.dict() - ) - result = await client.get_workflow_result(workflow_id=workflow_id, run_id=runId) - return {"workflow_id": workflow_id, "result": result} - except Exception as e: - # If engine connection or workflow run fails, a 500 error is raised - # The global_exception_handler ensures CORS headers are included. - raise HTTPException(status_code=500, detail="Failed to connect to Restack engine or run workflow.") - -# ./backend/src/services.py -import traceback -import asyncio -import time -from src.client import client -from src.functions.functions import generate_code, run_locally, validate_output -from src.workflows.workflow import AutonomousCodingWorkflow - -async def main(): - try: - await client.start_service( - workflows=[AutonomousCodingWorkflow], - functions=[generate_code, run_locally, validate_output], - ) - except Exception as e: - print(f"Error starting service: traceback: {traceback.format_exc()}") - print(f"Error starting service: {e}") - raise - -def run_services(): - try: - asyncio.run(main()) - except Exception as e: - print(f"Service failed: {e}") - # Keep the process alive for inspection - while True: - time.sleep(1) - -if __name__ == "__main__": - run_services() - -#./backend/src/client.py -import os -from restack_ai import Restack -from restack_ai.restack import CloudConnectionOptions - -RESTACK_TEMPORAL_ADDRESS = os.getenv('RESTACK_TEMPORAL_ADDRESS') -RESTACK_ENGINE_ADDRESS = os.getenv('RESTACK_ENGINE_ADDRESS') -RESTACK_ENGINE_ID = os.getenv('RESTACK_ENGINE_ID') -RESTACK_ENGINE_API_KEY = os.getenv('RESTACK_ENGINE_API_KEY') - -# src/client.py -connection_options = CloudConnectionOptions( - engine_id=RESTACK_ENGINE_ID, - api_key=RESTACK_ENGINE_API_KEY, - address=RESTACK_TEMPORAL_ADDRESS, - api_address=RESTACK_ENGINE_ADDRESS, - temporal_namespace="default") - -# Initialize Restack with these options options=connection_options -client = Restack(connection_options) - -# ./backend/src/prompts.py -# Store defaults here -default_generate_code_prompt = """You are an autonomous coding agent. - -The user prompt: {user_prompt} -The test conditions: {test_conditions} - -You must produce a Docker environment and code that meets the user's test conditions. - -**Additional Requirements**: -- Start by creating a `readme.md` file as your first file in the files array. This `readme.md` should begin with `#./readme.md` and contain: - - A brief summary of the user's prompt. - - A brief step-by-step plan of what you intend to do to meet the test conditions. -- Use a stable base Docker image: `FROM python:3.10-slim`. -- Install any necessary dependencies in the Dockerfile. -- Generate any configuration files (like `pyproject.toml` or `requirements.txt`) before the main Python files, if needed. -- Each file must start with `#./` on the first line. For example: - `#./main.py` - `print('hello world')` -- The Dockerfile should define an ENTRYPOINT that runs the main script or commands automatically so that running the container (e.g. `docker run ...`) immediately produces the final output required by the test conditions. -- Ensure the output visible on stdout fulfills the test conditions without further intervention. - -**Return JSON strictly matching this schema**: -{{ - "dockerfile": "", - "files": [ - {{ - "filename": "", - "content": "" - }}, - ... - ] -}} - -**Order of files**: -1. `readme.md` (with reasoning and plan) -2. Any configuration files (like `pyproject.toml` or `requirements.txt`) -3. Your main Python application files - -**Example**: -{{ - "dockerfile": "FROM python:3.10-slim\\n... ENTRYPOINT [\\"python3\\", \\"main.py\\"]", - "files": [ - {{ - "filename": "readme.md", - "content": "#./readme.md\\nThis is my reasoning..." - }}, - {{ - "filename": "pyproject.toml", - "content": "#./pyproject.toml\\n..." - }}, - {{ - "filename": "main.py", - "content": "#./main.py\\nprint('hello world')" - }} - ] -}} -""" - -default_validate_output_prompt = """The test conditions: {test_conditions} - -dockerfile: -{dockerfile} - -files: -{files_str} - -output: -{output} - -If all test conditions are met, return exactly: -{{ "result": true, "dockerfile": null, "files": null }} - -Otherwise (if you need to fix or add files, modify the dockerfile, etc.), return exactly: -{{ - "result": false, - "dockerfile": "FROM python:3.10-slim\\n...", - "files": [ - {{ - "filename": "filename.ext", - "content": "#./filename.ext\\n..." - }} - ] -}} - -You may add, remove, or modify multiple files as needed when returning false. Just ensure you follow the same schema and format strictly. Do not add extra commentary or keys. -If returning null for dockerfile or files, use JSON null, not a string.""" - -# Storing the current prompts in memory for simplicity. -current_generate_code_prompt = default_generate_code_prompt -current_validate_output_prompt = default_validate_output_prompt - -def get_prompts(): - return { - "generate_code_prompt": current_generate_code_prompt, - "validate_output_prompt": current_validate_output_prompt - } - -def set_prompts(generate_code_prompt: str, validate_output_prompt: str): - global current_generate_code_prompt, current_validate_output_prompt - current_generate_code_prompt = generate_code_prompt - current_validate_output_prompt = validate_output_prompt - -# ./backend/src/workflows/workflow.py -from restack_ai.workflow import workflow, import_functions, log -from dataclasses import dataclass -from datetime import timedelta -from datetime import datetime - -with import_functions(): - from src.functions.functions import generate_code, run_locally, validate_output - from src.functions.functions import GenerateCodeInput, RunCodeInput, ValidateOutputInput - -@dataclass -class WorkflowInputParams: - user_prompt: str - test_conditions: str - -@workflow.defn() -class AutonomousCodingWorkflow: - @workflow.run - async def run(self, input: WorkflowInputParams): - log.info("AutonomousCodingWorkflow started", input=input) - - gen_output = await workflow.step( - generate_code, - GenerateCodeInput( - user_prompt=input.user_prompt, - test_conditions=input.test_conditions - ), - start_to_close_timeout=timedelta(seconds=300) - ) - - dockerfile = gen_output.dockerfile - files = gen_output.files # list of {"filename":..., "content":...} - - iteration_count = 0 - max_iterations = 20 - - while iteration_count < max_iterations: - iteration_count += 1 - log.info(f"Iteration {iteration_count} start") - - run_output = await workflow.step( - run_locally, - RunCodeInput(dockerfile=dockerfile, files=files), - start_to_close_timeout=timedelta(seconds=300) - ) - - val_output = await workflow.step( - validate_output, - ValidateOutputInput( - dockerfile=dockerfile, - files=files, - output=run_output.output, - test_conditions=input.test_conditions - ), - start_to_close_timeout=timedelta(seconds=300) - ) - - if val_output.result: - log.info("AutonomousCodingWorkflow completed successfully") - return True - else: - changed_files = val_output.files if val_output.files else [] - if val_output.dockerfile: - dockerfile = val_output.dockerfile - - # Update the files list in-memory - for changed_file in changed_files: - changed_filename = changed_file["filename"] - changed_content = changed_file["content"] - found = False - for i, existing_file in enumerate(files): - if existing_file["filename"] == changed_filename: - files[i]["content"] = changed_content - found = True - break - if not found: - files.append({"filename": changed_filename, "content": changed_content}) - - log.warn("AutonomousCodingWorkflow reached max iterations without success") - return False - -# ./backend/src/functions/functions.py -from restack_ai.function import function, log -from dataclasses import dataclass -import os -import openai -import json -import shutil -import subprocess -from datetime import datetime - -from pydantic import BaseModel -from typing import List, Optional - -from src.prompts import current_generate_code_prompt, current_validate_output_prompt - -openai.api_key = os.environ.get("OPENAI_KEY") - -# Use the OpenAI Python SDK's structured output parsing -from openai import OpenAI -client = OpenAI(api_key=openai.api_key) - -class FileItem(BaseModel): - filename: str - content: str - - class Config: - extra = "forbid" - schema_extra = { - "type": "object", - "properties": { - "filename": {"type": "string"}, - "content": {"type": "string"} - }, - "required": ["filename", "content"], - "additionalProperties": False - } - -class GenerateCodeSchema(BaseModel): - dockerfile: str - files: List[FileItem] - - class Config: - extra = "forbid" - schema_extra = { - "type": "object", - "properties": { - "dockerfile": {"type": "string"}, - "files": { - "type": "array", - "items": {"$ref": "#/$defs/FileItem"} - } - }, - "required": ["dockerfile", "files"], - "additionalProperties": False, - "$defs": { - "FileItem": { - "type": "object", - "properties": { - "filename": {"type": "string"}, - "content": {"type": "string"} - }, - "required": ["filename", "content"], - "additionalProperties": False - } - } - } - -class ValidateOutputSchema(BaseModel): - result: bool - dockerfile: Optional[str] = None - files: Optional[List[FileItem]] = None - - class Config: - extra = "forbid" - schema_extra = { - "type": "object", - "properties": { - "result": {"type": "boolean"}, - "dockerfile": { - "anyOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "files": { - "anyOf": [ - { - "type": "array", - "items": {"$ref": "#/$defs/FileItem"} - }, - {"type": "null"} - ] - } - }, - "required": ["result", "dockerfile", "files"], - "additionalProperties": False, - "$defs": { - "FileItem": { - "type": "object", - "properties": { - "filename": {"type": "string"}, - "content": {"type": "string"} - }, - "required": ["filename", "content"], - "additionalProperties": False - } - } - } - - -@dataclass -class GenerateCodeInput: - user_prompt: str - test_conditions: str - -@dataclass -class GenerateCodeOutput: - dockerfile: str - files: list - -@function.defn() -async def generate_code(input: GenerateCodeInput) -> GenerateCodeOutput: - log.info("generate_code started", input=input) - - prompt = current_generate_code_prompt.format( - user_prompt=input.user_prompt, - test_conditions=input.test_conditions - ) - - completion = client.beta.chat.completions.parse( - model="gpt-4o-2024-08-06", - messages=[ - {"role": "system", "content": "You are the initial of an autonomous coding assistant agent. Generate complete code that will run."}, - {"role": "user", "content": prompt} - ], - response_format=GenerateCodeSchema - ) - - result = completion.choices[0].message - if result.refusal: - raise RuntimeError("Model refused to generate code.") - data = result.parsed - - files_list = [{"filename": f.filename, "content": f.content} for f in data.files] - - return GenerateCodeOutput(dockerfile=data.dockerfile, files=files_list) - - -@dataclass -class RunCodeInput: - dockerfile: str - files: list - -@dataclass -class RunCodeOutput: - output: str - @function.defn() async def run_locally(input: RunCodeInput) -> RunCodeOutput: log.info("run_locally started", input=input) @@ -641,16 +10,38 @@ async def run_locally(input: RunCodeInput) -> RunCodeOutput: run_folder = os.path.join(base_output_dir, f"llm_run_{timestamp}") os.makedirs(run_folder, exist_ok=True) + # Get the absolute path of the run folder for security checks + abs_run_folder = os.path.abspath(run_folder) + # Write the Dockerfile - dockerfile_path = os.path.join(run_folder, "Dockerfile") + dockerfile_path = os.path.abspath(os.path.join(run_folder, "Dockerfile")) + + # Ensure the Dockerfile path is within the run folder + if not dockerfile_path.startswith(abs_run_folder + os.sep) and dockerfile_path != abs_run_folder: + log.warning("Invalid Dockerfile path detected, potential path traversal") + return RunCodeOutput(output="Error: Invalid Dockerfile path") + with open(dockerfile_path, "w", encoding="utf-8") as f: f.write(input.dockerfile) # Write each file for file_item in input.files: - file_path = os.path.join(run_folder, file_item["filename"]) - os.makedirs(os.path.dirname(file_path), exist_ok=True) - with open(file_path, "w", encoding="utf-8") as ff: + # Construct the target file path + target_path = os.path.join(run_folder, file_item["filename"]) + + # Get the real absolute path of the target + abs_target_path = os.path.abspath(target_path) + + # Check if the target path is within the run folder + if not abs_target_path.startswith(abs_run_folder + os.sep) and abs_target_path != abs_run_folder: + log.warning(f"Path traversal attempt detected with filename: {file_item['filename']}") + continue + + # Create the directory structure if it doesn't exist + os.makedirs(os.path.dirname(abs_target_path), exist_ok=True) + + # Write the file content + with open(abs_target_path, "w", encoding="utf-8") as ff: ff.write(file_item["content"]) # Now run docker build, connecting to Docker-in-Docker at DOCKER_HOST @@ -665,52 +56,4 @@ async def run_locally(input: RunCodeInput) -> RunCodeOutput: if run_process.returncode != 0: return RunCodeOutput(output=run_process.stderr or run_process.stdout) - return RunCodeOutput(output=run_process.stdout) - - -@dataclass -class ValidateOutputInput: - dockerfile: str - files: list - output: str - test_conditions: str - -@dataclass -class ValidateOutputOutput: - result: bool - dockerfile: Optional[str] = None - files: Optional[list] = None - -@function.defn() -async def validate_output(input: ValidateOutputInput) -> ValidateOutputOutput: - log.info("validate_output started", input=input) - - files_str = json.dumps(input.files, indent=2) - - validation_prompt = current_validate_output_prompt.format( - test_conditions=input.test_conditions, - dockerfile=input.dockerfile, - files_str=files_str, - output=input.output - ) - - completion = client.beta.chat.completions.parse( - model="gpt-4o-2024-08-06", - messages=[ - {"role": "system", "content": "You are an iteration of an autonomous coding assistant agent. If you change any files, provide complete file content replacements. Append a brief explanation at the bottom of readme.md about what you tried."}, - {"role": "user", "content": validation_prompt} - ], - response_format=ValidateOutputSchema - ) - - result = completion.choices[0].message - if result.refusal: - return ValidateOutputOutput(result=False) - - data = result.parsed - updated_files = [{"filename": f.filename, "content": f.content} for f in data.files] if data.files is not None else None - - return ValidateOutputOutput(result=data.result, dockerfile=data.dockerfile, files=updated_files) - -HOW IT WORKS: -User inputs user_promp and test_conditions in the frontend UI then clicks "run workflow". The autonomous workflow begins, and iterates until the LLM deems that the test conditions are fulfilled. \ No newline at end of file + return RunCodeOutput(output=run_process.stdout) \ No newline at end of file