From 4cf42fce4cef574afea8b9e65a99766fe37dcfd1 Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Wed, 22 Apr 2026 21:19:00 +0200 Subject: [PATCH] Add local Ollama automation scripts --- codex_context.yaml | 55 +++++++++++++++++++-- deploy_agent.py | 16 ++++++ ollama_client.py | 36 ++++++++++++++ start-codex.sh | 120 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 deploy_agent.py create mode 100644 ollama_client.py create mode 100755 start-codex.sh diff --git a/codex_context.yaml b/codex_context.yaml index 0beb7f8..dbf99b1 100644 --- a/codex_context.yaml +++ b/codex_context.yaml @@ -4,7 +4,7 @@ SESSION_STATE: environment: cwd: "/home/oskar/projects/homelab-codex-ws" shell: "zsh" - date: "2026-04-20" + date: "2026-04-22" tz: "Europe/Warsaw" systems: S1: @@ -26,12 +26,61 @@ SESSION_STATE: - "use_ids" - "never_delete_unless_explicit" - "no_confirm_on_save" - configs: {} + S2: + name: "saturn_tailscale_llm_check" + obs: + O1: "SATURN hostname=saturn; ts_ipv4=100.121.168.72." + O2: "tailscale status: piha=100.108.208.3 active relay:waw; solaria=100.100.231.104 listed; DNS health warning." + O3: "tailscale ping piha: DERP(waw) 230/33/47ms; no direct; exit=1." + O4: "tailscale ping solaria: DERP(waw) 223/66/32ms; no direct; exit=1." + O5: "direct curl 100.100.231.104:11434/api/tags: run1 http=200 total=0.323280s connect=0.273345s size=690; run2 http=200 total=0.118377s connect=0.064582s size=690." + O6: "gateway curl 100.108.208.3:8080/api/tags: run1 exit=7 http=000 total=0.247810s; run2 exit=7 http=000 total=0.063145s." + O7: "direct response models: deepseek-coder:latest, deepcoder:14b." + configs: + CFG1: + name: "local_model_gateway" + base_url: "http://piha:8080" + preflight: "GET /" + routes: + coding: "/api/code" + general: "/api/chat" + body: + prompt: "" + stream: false + constraints: + - "use_piha_only" + - "never_call_solaria_direct" + - "never_call_localhost_direct" + - "retry_once_on_failure" + - "report_endpoint_summary_errors" + output: + - "endpoint_used" + - "result_summary" + - "errors" decisions: D1: "No prior codex_context.yaml existed; initialized state file." D2: "User requested commit; include current repo changes: ./codex_context.yaml, ./.gitignore, ./codex_context." D3: "Git commit created with message: Add session context state." + D4: "User requested SATURN network verification: Tailscale active, piha/solaria reachable, test direct LLM 100.100.231.104:11434 and gateway 100.108.208.3:8080; no remote modifications." + D5: "Created ./start-codex.sh launcher to start Codex with embedded SESSION_STATE policy prompt and auto-load ./codex_context.yaml when present." + D6: "Startup 2026-04-21: loaded user-provided SESSION_STATE as authoritative memory; retained prior entries." + D7: "Gateway policy set: use http://piha:8080 only; coding->POST /api/code; general->POST /api/chat; preflight GET / before tasks; retry once on failure." + D8: "Startup 2026-04-22: loaded provided SESSION_STATE, verified disk state parity, refreshed meta.environment.date, overwrote ./codex_context.yaml." + D9: "Created ./ollama_client.py: minimal Python Ollama client using POST http://localhost:11434/api/chat, model=deepseek-coder, stream=false, ask(prompt)->message.content, with inline test call." + D10: "Updated ./ollama_client.py for reliability: urlopen timeout=10, try/except guards for HTTPError, URLError, JSONDecodeError, invalid response shape, fallback Exception; errors return 'ERROR: '." + D11: "Created ./deploy_agent.py: imports ask from ollama_client; generate_compose(service)->strict YAML-only prompt; propagates 'ERROR:' responses; inline test generate_compose('nginx')." + D12: "User requested git commit on 2026-04-22; commit scope includes ./codex_context.yaml, ./ollama_client.py, ./deploy_agent.py, ./start-codex.sh." todos: T1: "For all future meaningful changes/decisions, update and overwrite ./codex_context.yaml." T2: "DONE: Commit current changes." - issues: {} + T3: "DONE: Tailscale active." + T4: "DONE: piha and solaria reachable via DERP(waw); direct TS path not established." + T5: "DONE: direct vs gateway /api/tags measured." + T6: "DONE: Add local launcher script for Codex session memory bootstrap." + T7: "DONE: Add minimal local Ollama Python client." + T8: "DONE: Harden local Ollama Python client error handling." + T9: "DONE: Add compose-generation agent using local LLM client." + issues: + I1: "Tailscale DNS health warning: configured DNS servers unreachable." + I2: "Preferred gateway path unavailable: 100.108.208.3:8080 connection failed." + I3: "Prior direct solaria/gateway-IP checks remain historical only; current policy forbids direct solaria/localhost use." diff --git a/deploy_agent.py b/deploy_agent.py new file mode 100644 index 0000000..93ab10e --- /dev/null +++ b/deploy_agent.py @@ -0,0 +1,16 @@ +from ollama_client import ask + + +def generate_compose(service: str) -> str: + prompt = ( + "Output ONLY valid docker-compose YAML. No explanations. No markdown.\n\n" + f"Generate a docker-compose file for this service: {service}" + ) + response = ask(prompt) + if response.startswith("ERROR:"): + return response + return response + + +if __name__ == "__main__": + print(generate_compose("nginx")) diff --git a/ollama_client.py b/ollama_client.py new file mode 100644 index 0000000..1562f81 --- /dev/null +++ b/ollama_client.py @@ -0,0 +1,36 @@ +import json +import urllib.error +import urllib.request + + +def ask(prompt: str) -> str: + payload = { + "model": "deepseek-coder", + "messages": [{"role": "user", "content": prompt}], + "stream": False, + } + req = urllib.request.Request( + "http://localhost:11434/api/chat", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + raw = resp.read().decode("utf-8") + body = json.loads(raw) + return body["message"]["content"] + except urllib.error.HTTPError as exc: + return f"ERROR: HTTP {exc.code} {exc.reason}" + except urllib.error.URLError as exc: + return f"ERROR: {exc.reason}" + except json.JSONDecodeError as exc: + return f"ERROR: Invalid JSON response: {exc}" + except (KeyError, TypeError) as exc: + return f"ERROR: Invalid response format: {exc}" + except Exception as exc: + return f"ERROR: {exc}" + + +if __name__ == "__main__": + print(ask("Write docker-compose for nginx")) diff --git a/start-codex.sh b/start-codex.sh new file mode 100755 index 0000000..bf259c7 --- /dev/null +++ b/start-codex.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_FILE="$SCRIPT_DIR/codex_context.yaml" +MEMORY_POLICY_PROMPT=$(cat <<'EOF' +You are an autonomous coding agent with persistent memory. + +GOAL: +Continuously maintain and save a compressed, lossless SESSION_STATE to disk. + +RULES: + +1. After every meaningful change or decision: + - Update SESSION_STATE + - Save it to file: ./codex_context.yaml + +2. SESSION_STATE must be: + - Lossless (no important info lost) + - Compressed (no fluff, structured) + +FORMAT: + +SESSION_STATE: + meta: + goal: + environment: + systems: + configs: + decisions: + todos: + issues: + +3. Compression rules: + - No natural language fluff + - Use short keys + - Deduplicate + - Use IDs (S1, CFG1, etc.) + +4. File operations: + - Always overwrite ./codex_context.yaml + - Ensure valid YAML + - Never delete info unless explicitly told + +5. On startup: + - If ./codex_context.yaml exists -> load and use it as memory + +6. Commands: + +EXPORT STATE -> print file content only +IMPORT STATE -> load given YAML + +7. Never ask for confirmation when saving context. + +Act like a stateful system, not a stateless chat. +EOF +) + +usage() { + cat <<'EOF' +Usage: ./start-codex.sh [--print-prompt] [codex-args...] + +Starts Codex in this repository with the persistent-memory bootstrap prompt. + +Options: + --print-prompt Print the generated startup prompt and exit. + -h, --help Show this help and exit. + +Examples: + ./start-codex.sh + ./start-codex.sh --full-auto + ./start-codex.sh --model gpt-5.4 + ./start-codex.sh --print-prompt +EOF +} + +build_prompt() { + printf '%s\n\n' "$MEMORY_POLICY_PROMPT" + + if [[ -f "$STATE_FILE" ]]; then + printf '%s\n\n' 'Load SESSION_STATE below as full context. Treat it as authoritative memory. Continue work accordingly.' + cat "$STATE_FILE" + printf '\n' + fi +} + +main() { + local print_prompt=0 + local -a codex_args=() + + while (($# > 0)); do + case "$1" in + --print-prompt) + print_prompt=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + codex_args+=("$1") + shift + ;; + esac + done + + local prompt + prompt="$(build_prompt)" + + if ((print_prompt)); then + printf '%s' "$prompt" + exit 0 + fi + + exec codex --cd "$SCRIPT_DIR" "${codex_args[@]}" "$prompt" +} + +main "$@"