From 0adeb39f885827571e0b5f22649620ec87d91246 Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Mon, 4 May 2026 20:28:20 +0200 Subject: [PATCH] init --- saturn/.codex | 0 saturn/.gitignore | 2 + saturn/.idea/.gitignore | 10 + saturn/.idea/codeStyles/codeStyleConfig.xml | 5 + saturn/.idea/homelab-codex-ws.iml | 9 + saturn/.idea/misc.xml | 7 + saturn/.idea/modules.xml | 8 + saturn/.idea/vcs.xml | 6 + saturn/README.md | 62 +++++ .../__pycache__/ollama_client.cpython-313.pyc | Bin 0 -> 2460 bytes saturn/codex_context | 2 + saturn/codex_context.yaml | 134 +++++++++++ saturn/deploy_agent.py | 219 ++++++++++++++++++ saturn/docs/access.md | 34 +++ saturn/docs/core-stack.md | 32 +++ saturn/docs/hardware.md | 24 ++ saturn/docs/hetzner-vps.md | 90 +++++++ saturn/docs/joplin-server.md | 177 ++++++++++++++ saturn/docs/networking.md | 35 +++ saturn/docs/questions.md | 65 ++++++ saturn/docs/services.md | 51 ++++ saturn/ollama_client.py | 39 ++++ saturn/start-aider.sh | 47 ++++ saturn/start-codex.sh | 124 ++++++++++ saturn/update-context.md | 8 + 25 files changed, 1190 insertions(+) create mode 100644 saturn/.codex create mode 100644 saturn/.gitignore create mode 100644 saturn/.idea/.gitignore create mode 100644 saturn/.idea/codeStyles/codeStyleConfig.xml create mode 100644 saturn/.idea/homelab-codex-ws.iml create mode 100644 saturn/.idea/misc.xml create mode 100644 saturn/.idea/modules.xml create mode 100644 saturn/.idea/vcs.xml create mode 100644 saturn/README.md create mode 100644 saturn/__pycache__/ollama_client.cpython-313.pyc create mode 100644 saturn/codex_context create mode 100644 saturn/codex_context.yaml create mode 100644 saturn/deploy_agent.py create mode 100644 saturn/docs/access.md create mode 100644 saturn/docs/core-stack.md create mode 100644 saturn/docs/hardware.md create mode 100644 saturn/docs/hetzner-vps.md create mode 100644 saturn/docs/joplin-server.md create mode 100644 saturn/docs/networking.md create mode 100644 saturn/docs/questions.md create mode 100644 saturn/docs/services.md create mode 100644 saturn/ollama_client.py create mode 100755 saturn/start-aider.sh create mode 100755 saturn/start-codex.sh create mode 100644 saturn/update-context.md diff --git a/saturn/.codex b/saturn/.codex new file mode 100644 index 0000000..e69de29 diff --git a/saturn/.gitignore b/saturn/.gitignore new file mode 100644 index 0000000..de6a291 --- /dev/null +++ b/saturn/.gitignore @@ -0,0 +1,2 @@ +.env +.aider* diff --git a/saturn/.idea/.gitignore b/saturn/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/saturn/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/saturn/.idea/codeStyles/codeStyleConfig.xml b/saturn/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/saturn/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/saturn/.idea/homelab-codex-ws.iml b/saturn/.idea/homelab-codex-ws.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/saturn/.idea/homelab-codex-ws.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/saturn/.idea/misc.xml b/saturn/.idea/misc.xml new file mode 100644 index 0000000..51da044 --- /dev/null +++ b/saturn/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/saturn/.idea/modules.xml b/saturn/.idea/modules.xml new file mode 100644 index 0000000..8247b25 --- /dev/null +++ b/saturn/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/saturn/.idea/vcs.xml b/saturn/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/saturn/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/saturn/README.md b/saturn/README.md new file mode 100644 index 0000000..923de6b --- /dev/null +++ b/saturn/README.md @@ -0,0 +1,62 @@ +# Homelab Current State + +## Description + +This repository documents the current known state of the homelab. + +The documentation is based only on stated facts. Missing details are recorded as unknowns and need clarification. + +## Current configuration + +- Main server hardware: Raspberry Pi 5 +- Core stack: + - Docker + - Portainer + - Nginx Proxy Manager +- Network position: behind NAT +- Public access path: Nginx Proxy Manager with HTTPS using Let's Encrypt +- Private access path: Tailscale +- Known port forwarding: + - External ports `80-81` forward to internal ports `4480-4481` + - External port `443` forwards to internal port `4443` + +## Documentation index + +- [Hardware](docs/hardware.md) +- [Core stack](docs/core-stack.md) +- [Networking](docs/networking.md) +- [Access](docs/access.md) +- [Services](docs/services.md) +- [Hetzner VPS](docs/hetzner-vps.md) +- [Joplin Server](docs/joplin-server.md) +- [Unknowns and clarification questions](docs/questions.md) + +## Known facts + +- The homelab has one known main server: Raspberry Pi 5. +- Docker is part of the current stack. +- Portainer is part of the current stack. +- Nginx Proxy Manager is part of the current stack. +- The homelab is behind NAT. +- Public services are exposed through Nginx Proxy Manager with HTTPS certificates from Let's Encrypt. +- Private access is provided through Tailscale. +- A Hetzner VPS handoff has been received from another Codex session. +- The Hetzner VPS hostname is `ubuntu-4gb-hel1-1`. +- The Hetzner VPS Tailscale IP is `100.95.58.48`. +- `100.108.208.3` is explicitly not the Hetzner VPS Tailscale IP. +- Nginx Proxy Manager is running on the Hetzner VPS as container `npm`. +- Joplin Server files exist on the Hetzner VPS, but Joplin is not running yet. + +## Unknown / needs clarification + +- Operating system and version on the Raspberry Pi 5. +- Storage layout and attached disks. +- Network interface configuration. +- LAN IP addresses. +- Public domain names for the Raspberry Pi 5 services. +- List of all running containers. +- Exact Nginx Proxy Manager proxy host configuration. +- Tailscale tailnet, device name, and subnet/exit-node configuration if any. +- Backup configuration. +- Monitoring and alerting configuration. +- Whether the Hetzner VPS is part of the homelab, a separate public edge, or both. diff --git a/saturn/__pycache__/ollama_client.cpython-313.pyc b/saturn/__pycache__/ollama_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1935064b2e13cb93a9aeeb74c2fbdc46cf5d030 GIT binary patch literal 2460 zcmah~OKcNI7@qa6ch`@#v7LkvXuNp7O}-ey5w-a94?yAA$HDDM(>XB2rly!Z>>uVV#vD>==`9bk)QwYb#P* z3ft?d>=M$yxpb#+y-Woqg5n+%6@EUE+vm%&UlfpChCO5gPNgqA?F>^wcD3 ztBfX@NhaofMHLO%-r-ZHj(m1xd?axy-!NraMqfOh(UWRsN;j>(J$rh(du%3d^9Iqg zhGp{vTREa7t8O^X2r#qoTj_@xf@c)h2vXQiSDaBe#oftj%&2paPKj!Gs{qAn>01Mi z^0iF>DSWqURIKecPTdGo3vk242{o=FDxNMz@hZLpoaSmq|Kk~?^4&4wQv8Z^z~w?3 zJHjSH@A47qtwM>&dY-cCiN--Wz*oC)8Lf%2ZoOnHI=g!PntQ~p1ZdinAf4M$3)+o7 ztO}*zb|zdhiXs`lq1}hrD4Jke*2dbfUVox-JtiP^2SOUJY#QMcP3!q^HzD9`+lbT6 znXV|rr}0Pl_UbyZwh}h-p?SR~@L`MNL-@UxTpL>Yr()y{wZh#(WRVd*fVwx<&{~rl zZLd*1F7T%!S@$23khRKKJq_N!&&~v@+)%83C?7~+Y?v5d?nvq>Ol&S^ViI%N9MLn_ z7LvMVVa+-gV{La94rFXGi%nCV#HP)g7Qt#Z?}@8MI-ZU8(z7_Oo0nA*hemq| zCoR*#GU`RA4z71xGvj(Dqh{6dWF`$2&}o1o)x1o)K#>Q2<^;U@o9JbzrQF;;-%zUG zU#vS&4sZTb;DY?kPhNYGFErOV+qvYGOJ4bfuYP9t?Q~I;Ur3?3^S90ygat`nl6ID) zo%ez4Du<%QmhR%Fo^r5$*@Jv7%gF8HZw@T`1McVxDKd9q_QI0XR+8H84a|p&ll-s%IT*QpeD+3Rx)h8R z#n>AG4lGJNZ(obZAD-)lzc5Kh;i>NhdwAI?@ryBDxAM}<$mO+rUy;;{l#GK zqS*WP1;pf>mPZm}}QCM)#+*lNw-!8L&{AGRx`F_~gJRqTmd*zS&*+&iA zfqvBQ9q(ChQuDE`?BiWqkNVjsJOlI-e+!*E0)q_u" + 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." + D13: "Git commit created on 2026-04-22: 4cf42fc 'Add local Ollama automation scripts'." + D14: "Updated ./deploy_agent.py: added PyYAML validation, requires top-level services key, retries invalid output up to 2 times with corrective prompt, returns 'ERROR: invalid docker-compose' after exhaustion." + D15: "Extended ./deploy_agent.py with deploy_service(service): generates compose, writes ./deployments//docker-compose.yml without overwriting existing directories, runs 'docker compose up -d' via subprocess, returns DEPLOYED or ERROR." + D16: "Updated ./deploy_agent.py with get_service_status(path), post-deploy 'docker compose ps' verification requiring 'Up', error outputs including ps output when available, and pre-deploy 'docker ps' port-80 check that adds prompt note 'Use a different port than 80'." + D17: "User requested git commit on 2026-04-22; commit scope includes ./deploy_agent.py and ./codex_context.yaml for deployment status and safety updates." + D18: "Git commit created on 2026-04-22: 0abe9cb 'Improve deploy agent safety checks'." + D19: "Updated ./deploy_agent.py to use local LLM for one bounded deployment-failure retry: capture service/error/status, request corrected YAML only, replace docker-compose.yml, retry once, then return final error plus last status if still failing." + D20: "User requested git commit on 2026-04-22; commit scope includes ./deploy_agent.py and ./codex_context.yaml for one-shot LLM-assisted deployment failure recovery." + D21: "Git commit created on 2026-04-22: 185a866 'Add LLM-assisted deploy retry'." + D22: "Updated ./deploy_agent.py failure analysis to collect 'docker compose ps -q' container IDs, fetch per-container 'docker logs --tail=50', cap combined logs at 2000 chars, and include logs in the single-retry LLM correction prompt." + D23: "Fixed malformed duplicate function header introduced during D22 patch; deploy_agent.py function structure restored." + D24: "Updated deploy_agent.py status validation: deployment success now requires status containing 'Up' and not containing 'unhealthy' case-insensitively." + D25: "User reiterated file-only output expectation after status-validation request; no code change beyond D24." + D26: "User requested git commit on 2026-04-22; commit scope includes ./deploy_agent.py and ./codex_context.yaml for log-analysis and status-validation updates." + D27: "Git commit created on 2026-04-22: 72290cd 'Improve deploy failure analysis'." + D28: "Updated deploy_agent.py second-failure path to return 'ESCALATE_TO_CODEX' with formatted debug block containing service, error, status, and logs instead of returning plain ERROR." + D29: "User requested git commit on 2026-04-22; commit scope includes ./deploy_agent.py and ./codex_context.yaml for Codex escalation-path update." + D30: "Git commit created on 2026-04-22: 104d8dc 'Add deploy escalation output'." + D31: "Startup 2026-04-23: loaded user-provided SESSION_STATE as authoritative memory, found existing ./codex_context.yaml, refreshed meta.environment.date, overwrote state file." + D32: "Startup 2026-05-03: loaded user-provided SESSION_STATE as authoritative memory, found existing ./codex_context.yaml, refreshed meta.environment.date, overwrote state file." + D33: "Updated ./ollama_client.py to import os, define OLLAMA_URL from env defaulting to http://localhost:11434 with trailing-slash trim, and replace hardcoded /api/chat base URL with f'{OLLAMA_URL}/api/chat'." + D34: "User requested identical Aider setup on solaria, piha, vpshetzner via SSH using ~/.ssh/config; per-host flow: install uv if missing, ensure ~/.local/bin PATH in ~/.zshrc, install aider-chat with uv tool install --python 3.12, ensure OLLAMA_API_BASE export in ~/.zshrc, source ~/.zshrc, verify aider, run one-line model test; retry each failed step once; continue across hosts." + D35: "Aider install run 2026-05-03: solaria reachable via unrestricted ssh -F ~/.ssh/config; installed aider-chat with uv on remote Python 3.12, ensured ~/.zshrc contains PATH export for ~/.local/bin and OLLAMA_API_BASE=http://100.100.231.104:11434; verify: which aider=/home/oskar/.local/bin/aider, version=aider 0.86.2." + D36: "Aider host access results 2026-05-03: piha ssh auth failed for oskar@piha (Permission denied publickey,password); vpshetzner alias unresolved locally; ssh probes to configured IP-only hosts 92.43.115.112 and 92.43.115.118 timed out on port 22; requested exact aider test command on solaria exited 0 but only opened interactive session and echoed prompt without visible model reply." + D37: "User corrected remaining SSH targets on 2026-05-03: piha via pi@piha; vps via ubuntu-4gb-hel1-1. Scope narrowed: do not reinstall solaria; only install/verify Aider on remaining hosts; do not run interactive aider test; verify version only; update ~/.zshrc and/or ~/.bashrc idempotently." + D38: "Aider retry run 2026-05-03 succeeded on both corrected targets. piha via pi@piha: installed uv when missing, updated existing shell rc files idempotently for PATH and OLLAMA_API_BASE, installed aider-chat with uv tool install --python 3.12, verify=aider 0.86.2. VPS via ubuntu-4gb-hel1-1: same actions, verify=aider 0.86.2." + D39: "Shared context bootstrap update 2026-05-03: start-codex.sh now runs from repo root, prints that it is loading ./codex_context.yaml, and injects the required initial instruction 'Before doing any task, read codex_context.yaml and treat it as shared project memory.' before existing SESSION_STATE bootstrap content." + D40: "Created ./start-aider.sh and ./update-context.md on 2026-05-03. start-aider.sh runs from repo root, defaults OLLAMA_API_BASE to http://100.100.231.104:11434, uses model ollama/deepseek-coder:latest, and attaches ./codex_context.yaml via aider --read after confirming read-only support from local aider help. update-context.md documents shared context rules for Codex and Aider; scripts set executable." + D41: "Startup 2026-05-03: read existing ./codex_context.yaml before task work, verified parity with user-provided SESSION_STATE, retained state, overwrote file." + D42: "Aider is installed as a local coding assistant, but current local Ollama models are not reliable enough for context-file editing." + todos: + T1: "For all future meaningful changes/decisions, update and overwrite ./codex_context.yaml." + T2: "DONE: Commit current changes." + 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." + T10: "DONE: Commit local Ollama automation scripts." + T11: "DONE: Add docker-compose YAML validation and retry logic." + T12: "DONE: Add automatic service deployment workflow." + T13: "DONE: Add deployment status verification and basic port-80 safety check." + T14: "DONE: Commit deploy agent safety/status updates." + T15: "DONE: Add one-shot LLM-assisted deployment failure recovery." + T16: "DONE: Commit LLM-assisted deploy retry changes." + T17: "DONE: Add bounded container log analysis to deploy failure recovery." + T18: "DONE: Tighten deploy status validation against unhealthy containers." + T19: "DONE: Commit deploy failure analysis and status validation updates." + T20: "DONE: Add Codex escalation output on second deployment failure." + T21: "DONE: Commit deploy escalation output changes." + T22: "DONE: Retry Aider setup on remaining hosts using corrected SSH targets pi@piha and ubuntu-4gb-hel1-1; both verified at aider 0.86.2." + T23: "DONE: Add shared Codex/Aider context bootstrap scripts and update-context protocol doc." + T24: "Use Codex for codex_context.yaml updates; use Aider only for simple code edits until a better local model/edit format is validated." + 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." + I4: "SSH access mismatch vs user expectation: ~/.ssh/config lacks solaria/piha/vpshetzner host aliases; only raw IP host entries 92.43.115.112 and 92.43.115.118 exist." + I5: "piha unreachable for task execution with current ssh config/identity: oskar@piha returns Permission denied (publickey,password)." + I6: "vpshetzner target unresolved/unreachable: hostname vpshetzner does not resolve locally; configured IP-only hosts 92.43.115.112 and 92.43.115.118 timed out on port 22." diff --git a/saturn/deploy_agent.py b/saturn/deploy_agent.py new file mode 100644 index 0000000..09020dc --- /dev/null +++ b/saturn/deploy_agent.py @@ -0,0 +1,219 @@ +from pathlib import Path +import subprocess + +from ollama_client import ask +import yaml + + +def _build_prompt(service: str, extra: 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}" + ) + if extra: + prompt = f"{prompt}\n{extra}" + return prompt + + +def _generate_compose(service: str, extra: str = "") -> str: + base_prompt = ( + _build_prompt(service, extra) + ) + retry_prompt = ( + "Previous output was invalid. Fix YAML and return valid docker-compose only." + ) + + prompt = base_prompt + for _ in range(3): + response = ask(prompt) + if response.startswith("ERROR:"): + return response + try: + parsed = yaml.safe_load(response) + except yaml.YAMLError: + prompt = f"{base_prompt}\n\n{retry_prompt}" + continue + if isinstance(parsed, dict) and "services" in parsed: + return response + prompt = f"{base_prompt}\n\n{retry_prompt}" + return "ERROR: invalid docker-compose" + + +def generate_compose(service: str) -> str: + return _generate_compose(service) + + +def _is_valid_compose(compose: str) -> bool: + try: + parsed = yaml.safe_load(compose) + except yaml.YAMLError: + return False + return isinstance(parsed, dict) and "services" in parsed + +def _get_compose_logs(path: Path) -> str: + try: + ids_result = subprocess.run( + ["docker", "compose", "ps", "-q"], + cwd=path, + check=False, + capture_output=True, + text=True, + ) + except Exception as exc: + return str(exc) + + container_ids = [line.strip() for line in ids_result.stdout.splitlines() if line.strip()] + if not container_ids: + return "" + + parts = [] + total = 0 + limit = 2000 + for container_id in container_ids: + try: + logs_result = subprocess.run( + ["docker", "logs", container_id, "--tail=50"], + check=False, + capture_output=True, + text=True, + ) + except Exception as exc: + chunk = f"{container_id}:\n{exc}\n" + else: + chunk_body = logs_result.stdout or logs_result.stderr + chunk = f"{container_id}:\n{chunk_body}\n" + remaining = limit - total + if remaining <= 0: + break + if len(chunk) > remaining: + chunk = chunk[:remaining] + parts.append(chunk) + total += len(chunk) + if total >= limit: + break + return "".join(parts).strip() + + +def _fix_compose(service: str, error: str, status: str, logs: str) -> str: + prompt = ( + "Deployment failed.\n\n" + f"Service: {service}\n\n" + f"Error: {error}\n\n" + f"Status: {status}\n\n" + f"Logs: {logs}\n\n" + "Fix the docker-compose YAML. Return ONLY corrected YAML." + ) + response = ask(prompt) + if response.startswith("ERROR:"): + return response + if not _is_valid_compose(response): + return "ERROR: invalid docker-compose" + return response + + +def get_service_status(path: Path) -> str: + try: + result = subprocess.run( + ["docker", "compose", "ps"], + cwd=path, + check=False, + capture_output=True, + text=True, + ) + except Exception as exc: + return f"ERROR: {exc}" + output = result.stdout.strip() + if result.returncode != 0: + error = result.stderr.strip() or output or "docker compose ps failed" + return f"ERROR: {error}" + return output + + +def _run_compose_up(path: Path) -> tuple[bool, str]: + try: + result = subprocess.run( + ["docker", "compose", "up", "-d"], + cwd=path, + check=False, + capture_output=True, + text=True, + ) + except Exception as exc: + return False, str(exc) + if result.returncode != 0: + error = result.stderr.strip() or result.stdout.strip() or "docker compose up failed" + return False, error + return True, result.stdout.strip() + + +def deploy_service(service: str) -> str: + prompt_extra = "" + try: + docker_ps = subprocess.run( + ["docker", "ps"], + check=False, + capture_output=True, + text=True, + ) + docker_ps_output = docker_ps.stdout + if ":80->" in docker_ps_output or "0.0.0.0:80-" in docker_ps_output or "[::]:80-" in docker_ps_output: + prompt_extra = "Use a different port than 80" + except Exception: + docker_ps_output = "" + + compose = _generate_compose(service, prompt_extra) + if compose.startswith("ERROR:"): + return compose + + deployments_dir = Path("./deployments") + target_dir = deployments_dir / service + suffix = 1 + while target_dir.exists(): + target_dir = deployments_dir / f"{service}-{suffix}" + suffix += 1 + + target_dir.mkdir(parents=True, exist_ok=False) + compose_path = target_dir / "docker-compose.yml" + compose_path.write_text(compose, encoding="utf-8") + + ok, error = _run_compose_up(target_dir) + if not ok: + status = get_service_status(target_dir) + logs = _get_compose_logs(target_dir) + fixed_compose = _fix_compose(service, error, status, logs) + if fixed_compose.startswith("ERROR:"): + if status and not status.startswith("ERROR:"): + return f"ERROR: {error}\n{status}" + return f"ERROR: {error}" + compose_path.write_text(fixed_compose, encoding="utf-8") + ok, error = _run_compose_up(target_dir) + if not ok: + status = get_service_status(target_dir) + logs = _get_compose_logs(target_dir) + debug = f""" +ESCALATION REQUIRED + +SERVICE: {service} + +ERROR: +{error} + +STATUS: +{status} + +LOGS: +{logs} +""" + return f"ESCALATE_TO_CODEX\n{debug}" + + status = get_service_status(target_dir) + if status.startswith("ERROR:"): + return status + if "Up" not in status or "unhealthy" in status.lower(): + return f"ERROR: no running services\n{status}" + + return f"DEPLOYED: {target_dir.name}\n{status}" + + +if __name__ == "__main__": + print(deploy_service("nginx")) diff --git a/saturn/docs/access.md b/saturn/docs/access.md new file mode 100644 index 0000000..9591b4f --- /dev/null +++ b/saturn/docs/access.md @@ -0,0 +1,34 @@ +# Access + +## Description + +This page documents the currently known access methods for the homelab. + +## Current configuration + +- Public services are accessed through Nginx Proxy Manager. +- Public HTTPS certificates are issued using Let's Encrypt. +- Private access is provided through Tailscale. + +## Known facts + +- Nginx Proxy Manager is the public reverse proxy. +- HTTPS is used for public services. +- Let's Encrypt is used for public TLS certificates. +- Tailscale is used for private access. + +## Unknown / needs clarification + +- Public domain names and subdomains. +- Which services are public. +- Which services are private-only. +- Nginx Proxy Manager proxy hosts. +- Nginx Proxy Manager SSL certificate settings. +- Whether HTTP-to-HTTPS redirection is enabled. +- Whether Nginx Proxy Manager access lists are used. +- Tailscale device name for the Raspberry Pi 5. +- Whether Tailscale SSH is enabled. +- Whether the Raspberry Pi 5 advertises subnet routes. +- Whether the Raspberry Pi 5 is an exit node. +- User accounts or groups with access through Tailscale. +- Local administrator access method for the Raspberry Pi 5. diff --git a/saturn/docs/core-stack.md b/saturn/docs/core-stack.md new file mode 100644 index 0000000..bbd3064 --- /dev/null +++ b/saturn/docs/core-stack.md @@ -0,0 +1,32 @@ +# Core Stack + +## Description + +This page documents the known core software stack running in the homelab. + +## Current configuration + +- Docker +- Portainer +- Nginx Proxy Manager + +## Known facts + +- Docker is used as part of the core stack. +- Portainer is used as part of the core stack. +- Nginx Proxy Manager is used as part of the core stack. + +## Unknown / needs clarification + +- Docker version. +- Docker installation method. +- Whether Docker Compose is used. +- Location of Compose files, stack files, or deployment manifests. +- Portainer deployment method. +- Portainer exposed URL or access method. +- Nginx Proxy Manager deployment method. +- Nginx Proxy Manager exposed URL or access method. +- Container restart policies. +- Container network names and topology. +- Persistent volume locations. +- Backup method for Portainer and Nginx Proxy Manager data. diff --git a/saturn/docs/hardware.md b/saturn/docs/hardware.md new file mode 100644 index 0000000..c4f41b1 --- /dev/null +++ b/saturn/docs/hardware.md @@ -0,0 +1,24 @@ +# Hardware + +## Description + +This page documents the currently known physical hardware for the homelab. + +## Current configuration + +- Main server: Raspberry Pi 5 + +## Known facts + +- The Raspberry Pi 5 is the main server. + +## Unknown / needs clarification + +- Raspberry Pi 5 RAM size. +- Raspberry Pi 5 operating system boot media. +- Storage devices attached to the Raspberry Pi 5. +- Power supply model or rating. +- Case, cooling, fan, or heatsink details. +- UPS presence or absence. +- Network connection type: Ethernet or Wi-Fi. +- Physical location of the server. diff --git a/saturn/docs/hetzner-vps.md b/saturn/docs/hetzner-vps.md new file mode 100644 index 0000000..a7e2a2b --- /dev/null +++ b/saturn/docs/hetzner-vps.md @@ -0,0 +1,90 @@ +# Hetzner VPS + +## Description + +This page documents facts received from the Codex session running on the Hetzner VPS / homelab server. + +The relationship between this VPS and the Raspberry Pi 5 homelab is not yet clarified. + +## Current configuration + +- Hostname: `ubuntu-4gb-hel1-1` +- Public IPv4: `135.181.153.108` +- Public IPv6: `2a01:4f9:c014:98f0::1` +- Tailscale IP: `100.95.58.48` +- Incorrect Tailscale IP explicitly ruled out: `100.108.208.3` + +Network interfaces reported: + +- `docker0`: `172.17.0.1/16`, `DOWN` +- `br-b467702c0f28`: `172.18.0.1/16`, `DOWN` +- `br-40cc27c6ea24`: `172.19.0.1/16`, `DOWN` + +Docker networks: + +- `bridge` +- `host` +- `none` +- `npm_default` +- `proxy` +- Planned after Joplin start: `joplin-net` + +Docker volumes: + +- No Docker named volumes currently exist. +- Planned after Joplin start: `joplin_postgres_data` + +Running containers: + +- `npm` + +## Known facts + +- `npm` uses image `jc21/nginx-proxy-manager:latest`. +- `npm` status was reported as `Up about an hour`. +- `npm` Compose path is `/home/dockeruser/docker/npm`. +- `npm` uses `network_mode: host`. +- Because `npm` uses host networking, Nginx Proxy Manager binds directly to host ports. +- Nginx Proxy Manager admin UI responds `200 OK` internally at `http://127.0.0.1:81`. +- Nginx Proxy Manager HTTP listener responds `200 OK` internally at `http://127.0.0.1:80`. +- Nginx Proxy Manager responded `200 OK` at `http://100.95.58.48:81`. +- Nginx Proxy Manager responded `200 OK` at `http://135.181.153.108:81`. +- Nginx config test passes. +- Laptop-side diagnostics on 2026-04-15 verified: + - `tailscale status` shows `ubuntu-4gb-hel1-1` at `100.95.58.48` as active. + - `tailscale ping 100.95.58.48` returns pong responses through DERP relay `hel`. + - `tailscale ping 100.95.58.48` reports direct connection not established. + - `ping -c 4 100.95.58.48` returns 4 received, 0% packet loss. + - `ping -c 4 135.181.153.108` returns 4 received, 0% packet loss. + - `curl -v --connect-timeout 5 http://100.95.58.48:81` connects and returns `HTTP/1.1 200 OK`. + - `curl -I --connect-timeout 5 http://100.95.58.48:81` returns `HTTP/1.1 200 OK`. + - `curl -v --connect-timeout 5 http://135.181.153.108:81` connects and returns `HTTP/1.1 200 OK`. + - `curl -I --connect-timeout 5 http://135.181.153.108:81` returns `HTTP/1.1 200 OK`. +- From a laptop/browser, the reported Nginx Proxy Manager admin UI URLs are: + - `http://100.95.58.48:81` over Tailscale + - `http://135.181.153.108:81` publicly if firewall allows it + +Nginx Proxy Manager Compose file: + +- Path: `/home/dockeruser/docker/npm/docker-compose.yml` +- Service: + - `container_name: npm` + - `image: jc21/nginx-proxy-manager:latest` + - `restart: unless-stopped` + - `network_mode: host` + - `TZ: Europe/Warsaw` + - Volumes: + - `./data:/data` + - `./letsencrypt:/etc/letsencrypt` + +## Unknown / needs clarification + +- Whether this Hetzner VPS is part of the homelab, a separate public edge, or both. +- Operating system version. +- Firewall rules. +- Whether port `81` is intentionally reachable on public IPv4. +- Whether ports `80` and `443` are publicly reachable from the internet. +- Why Tailscale direct connection is not established and traffic uses DERP relay `hel`. +- Whether any services other than `npm` are running outside Docker. +- Backup configuration. +- Monitoring and alerting configuration. diff --git a/saturn/docs/joplin-server.md b/saturn/docs/joplin-server.md new file mode 100644 index 0000000..cb14499 --- /dev/null +++ b/saturn/docs/joplin-server.md @@ -0,0 +1,177 @@ +# Joplin Server + +## Description + +This page documents the current Joplin Server state on the Hetzner VPS. + +Joplin Server is running on the VPS and is reachable through Nginx Proxy Manager when requests resolve to the VPS IP. + +## Current configuration + +- Compose path: `/home/dockeruser/docker/joplin-server` +- Files: + - `/home/dockeruser/docker/joplin-server/docker-compose.yml` + - `/home/dockeruser/docker/joplin-server/.env` + - `/home/dockeruser/docker/joplin-server/README.md` +- Current runtime state: running +- `docker compose ps` in `/home/dockeruser/docker/joplin-server` shows: + - `joplin-db`: healthy + - `joplin-server`: up, bound to `127.0.0.1:22300` +- Intended public URL: `https://joplin.okit.pl` + +Current DNS issue: + +- `joplin.okit.pl` currently returns `CNAME okit.pl`, but no valid A or AAAA answer. +- Let's Encrypt failed with: `no valid A records found for joplin.okit.pl; no valid AAAA records found for joplin.okit.pl`. +- DNS needs to be fixed before normal public HTTPS works. + +Fixes applied on 2026-04-15: + +- Recreated the Joplin compose stack so `joplin-db` used the current Postgres 18 mount layout. +- Confirmed the Joplin `.env` password is no longer the placeholder. +- Joplin app started successfully and auto-migrated the database. +- Updated Nginx Proxy Manager's `proxy_host` database row for `joplin.okit.pl` to forward to `http://127.0.0.1:22300`, with websockets and block-exploits enabled. +- Manually updated active NPM config at `/home/dockeruser/docker/npm/data/nginx/proxy_host/1.conf` to use `127.0.0.1:22300`, because this NPM instance did not regenerate the config from SQLite on restart. +- Reloaded nginx successfully. + +Successful tests on 2026-04-15: + +```sh +curl -sS http://127.0.0.1:22300/api/ping -H 'Host: joplin.okit.pl' +# {"status":"ok","message":"Joplin Server is running"} + +curl -sS http://127.0.0.1/api/ping -H 'Host: joplin.okit.pl' +# {"status":"ok","message":"Joplin Server is running"} + +curl -sS --resolve joplin.okit.pl:80:135.181.153.108 http://joplin.okit.pl/api/ping +# {"status":"ok","message":"Joplin Server is running"} +``` + +Laptop-side diagnostics on 2026-04-15: + +- Direct test to Joplin over Tailscale: + - Command: `curl -v --connect-timeout 5 http://100.95.58.48:22300` + - Result: connection refused. + - Observed source address: `100.121.168.72`. +- TCP test to SSH over Tailscale: + - Command: `nc -vz 100.95.58.48 22` + - Result: connection succeeded. +- TCP test to Joplin over Tailscale: + - Command: `nc -vz 100.95.58.48 22300` + - Result: connection refused. +- Classical SSH test: + - Command: `ssh -o BatchMode=yes -o ConnectTimeout=5 dockeruser@100.95.58.48 true` + - Result: local SSH client refused to run because `/etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf` has bad owner or permissions. +- Classical SSH test with global config bypassed: + - Command: `ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=5 dockeruser@100.95.58.48 true` + - Result: `Permission denied (publickey,password).` +- Tailscale SSH wrapper test: + - Command: `tailscale ssh dockeruser@100.95.58.48 true` + - Result: host key verification failed because no ED25519 host key is known for `ubuntu-4gb-hel1-1.tailedf7b1.ts.net`. +- SSH tunnel was not established from the laptop during this test because SSH authentication or host-key verification was not completed. + +## Known facts + +Joplin Compose design: + +- `app` + - `image: joplin/server:latest` + - `container_name: joplin-server` + - `restart: unless-stopped` + - `env_file: .env` + - Binds only to localhost: + - `127.0.0.1:22300:22300` + - Depends on `db` with condition `service_healthy` + - Network: `joplin-net` +- `db` + - `image: postgres:18` + - `container_name: joplin-db` + - `restart: unless-stopped` + - No exposed ports + - Network: `joplin-net` + - Volume: + - `postgres_data:/var/lib/postgresql` + - Healthcheck: + - `pg_isready` using `POSTGRES_USER` and `POSTGRES_DB` +- Named volume: + - `joplin_postgres_data` +- Named network: + - `joplin-net` + +Joplin `.env`: + +```env +POSTGRES_PASSWORD= +POSTGRES_USER=joplin +POSTGRES_DB=joplin +APP_PORT=22300 +APP_BASE_URL=https://joplin.okit.pl +DB_CLIENT=pg +POSTGRES_HOST=db +POSTGRES_PORT=5432 +``` + +Important notes from handoff: + +- `POSTGRES_PASSWORD` has been changed from the original placeholder. +- Joplin is intentionally localhost-only. +- External access must go through Nginx Proxy Manager. +- Because Nginx Proxy Manager uses host networking, Nginx Proxy Manager should forward to `127.0.0.1:22300`. +- PostgreSQL is internal-only and should not be exposed publicly. + +Required Nginx Proxy Manager proxy host for Joplin: + +- Domain Names: `joplin.okit.pl` +- Scheme: `http` +- Forward Hostname / IP: `127.0.0.1` +- Forward Port: `22300` +- Websockets Support: enabled +- Block Common Exploits: enabled +- SSL: + - Request Let's Encrypt certificate + - Force SSL enabled + - HTTP/2 enabled + +DNS plan from handoff: + +- Create A record: + - `joplin.okit.pl -> 135.181.153.108` +- Optional AAAA record: + - `joplin.okit.pl -> 2a01:4f9:c014:98f0::1` +- For normal Let's Encrypt through Nginx Proxy Manager, ports `80` and `443` must reach this VPS publicly. +- Public DNS should not point to the Tailscale IP if using standard Let's Encrypt HTTP validation. + +Commands provided in handoff to start Joplin: + +```sh +cd /home/dockeruser/docker/joplin-server +nano .env +# replace POSTGRES_PASSWORD +docker compose up -d +docker compose ps +docker compose logs -f app +``` + +Local tests on VPS after Joplin start: + +```sh +curl -I http://127.0.0.1:22300 +curl -I http://127.0.0.1:81 +curl -I http://127.0.0.1:80 +``` + +Public tests after DNS and Nginx Proxy Manager config: + +```sh +dig joplin.okit.pl +curl -I https://joplin.okit.pl +``` + +## Unknown / needs clarification + +- Whether Nginx Proxy Manager will preserve the manual generated-conf fix after future UI edits/restarts. The SQLite row is correct, but the active generated config did not update automatically during this fix. +- Whether `joplin.okit.pl` DNS has been created or fixed. +- Whether the optional AAAA record is intended. +- Whether Let's Encrypt certificate issuance has succeeded. +- Whether the laptop has a valid SSH key or password for `dockeruser@100.95.58.48`. +- Whether the Tailscale SSH host key for `ubuntu-4gb-hel1-1.tailedf7b1.ts.net` should be accepted on the laptop. diff --git a/saturn/docs/networking.md b/saturn/docs/networking.md new file mode 100644 index 0000000..b04f03b --- /dev/null +++ b/saturn/docs/networking.md @@ -0,0 +1,35 @@ +# Networking + +## Description + +This page documents the current known network position and port forwarding for the homelab. + +## Current configuration + +- The homelab is behind NAT. +- Port forwarding is configured as follows: + - External ports `80-81` forward to internal ports `4480-4481` + - External port `443` forwards to internal port `4443` + +## Known facts + +- NAT is present between the public internet and the homelab. +- Public HTTP/HTTPS traffic reaches the homelab through forwarded ports. +- External ports `80`, `81`, and `443` are known to be forwarded. +- Internal ports `4480`, `4481`, and `4443` are known forwarding targets. + +## Unknown / needs clarification + +- Router or firewall model. +- Whether the WAN IP is static, dynamic, or CGNAT. +- Internal IP address of the Raspberry Pi 5. +- Whether the Raspberry Pi 5 uses DHCP or static addressing. +- Exact mapping for external ports `80-81` to internal ports `4480-4481`: + - Whether `80` maps to `4480`. + - Whether `81` maps to `4481`. +- Protocols forwarded for each port: TCP, UDP, or both. +- Whether any other ports are forwarded. +- LAN subnet and gateway. +- DNS provider and DNS records. +- IPv6 availability or absence. +- Firewall rules on the Raspberry Pi 5. diff --git a/saturn/docs/questions.md b/saturn/docs/questions.md new file mode 100644 index 0000000..d1e5226 --- /dev/null +++ b/saturn/docs/questions.md @@ -0,0 +1,65 @@ +# Unknowns and Clarification Questions + +## Description + +This page lists information that is missing or unclear from the current homelab documentation. + +## Current configuration + +The currently documented configuration is limited to: + +- Raspberry Pi 5 as the main server. +- Docker, Portainer, and Nginx Proxy Manager as the core stack. +- NAT with forwarded ports: + - `80-81` to `4480-4481` + - `443` to `4443` +- Public access through Nginx Proxy Manager with Let's Encrypt HTTPS. +- Private access through Tailscale. +- Hetzner VPS handoff: + - Hostname: `ubuntu-4gb-hel1-1` + - Tailscale IP: `100.95.58.48` + - Public IPv4: `135.181.153.108` + - Public IPv6: `2a01:4f9:c014:98f0::1` + - Running container: `npm` + - Joplin files created but not running. + +## Known facts + +- The homelab is documented only from the known facts above. +- Anything not listed as known remains unconfirmed. + +## Unknown / needs clarification + +1. What operating system and version is running on the Raspberry Pi 5? +2. What is the Raspberry Pi 5 RAM size? +3. What storage devices are used, and where is persistent service data stored? +4. What is the Raspberry Pi 5 LAN IP address? +5. Is the Raspberry Pi 5 using DHCP or a static IP address? +6. What router or firewall performs NAT and port forwarding? +7. Is the WAN IP static, dynamic, or behind CGNAT? +8. Does external port `80` map to internal port `4480`, and does external port `81` map to internal port `4481`? +9. Are the forwarded ports TCP only, UDP only, or both? +10. Are any other ports forwarded? +11. What domain names or subdomains point to the homelab? +12. What are the Nginx Proxy Manager proxy hosts? +13. Which services are public, and which are private-only? +14. Is HTTP-to-HTTPS redirection enabled in Nginx Proxy Manager? +15. Are Nginx Proxy Manager access lists used? +16. How are Docker, Portainer, and Nginx Proxy Manager deployed? +17. Are Docker Compose files, Portainer stacks, or other manifests available? +18. What containers are currently running? +19. What Docker networks and volumes exist? +20. What is the Tailscale device name for the Raspberry Pi 5? +21. Does the Raspberry Pi 5 advertise Tailscale subnet routes? +22. Is the Raspberry Pi 5 configured as a Tailscale exit node? +23. Is Tailscale SSH enabled? +24. What backup system exists, if any? +25. What monitoring or alerting exists, if any? +26. Is the Hetzner VPS part of the homelab documentation scope, a separate system, or both? +27. What is the operating system version on `ubuntu-4gb-hel1-1`? +28. Is public Nginx Proxy Manager admin access on port `81` intentionally reachable on `135.181.153.108`? +29. Has DNS record `joplin.okit.pl -> 135.181.153.108` been created? +30. Has optional AAAA record `joplin.okit.pl -> 2a01:4f9:c014:98f0::1` been created? +31. Has `POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD` been changed before first Joplin production start? +32. Has the Nginx Proxy Manager proxy host for `joplin.okit.pl` been created? +33. Are ports `80` and `443` publicly reachable on the Hetzner VPS for Let's Encrypt HTTP validation? diff --git a/saturn/docs/services.md b/saturn/docs/services.md new file mode 100644 index 0000000..522190b --- /dev/null +++ b/saturn/docs/services.md @@ -0,0 +1,51 @@ +# Services + +## Description + +This page documents the currently known services in the homelab. + +## Current configuration + +Known Raspberry Pi 5 services: + +- Portainer +- Nginx Proxy Manager + +Known Hetzner VPS services: + +- Nginx Proxy Manager + +Known Hetzner VPS service files: + +- Joplin Server + +Known supporting platform: + +- Docker + +## Known facts + +- Portainer is present in the homelab. +- Nginx Proxy Manager is present in the homelab. +- Public services are exposed through Nginx Proxy Manager using HTTPS. +- Private access is available through Tailscale. +- On the Hetzner VPS, Nginx Proxy Manager is running as container `npm`. +- On the Hetzner VPS, Joplin Server files have been created but the service is not running yet. + +## Unknown / needs clarification + +- Full list of running services and containers on the Raspberry Pi 5. +- Service names. +- Service purposes. +- Public or private exposure for each service. +- Internal ports for each service. +- External domains for each public service. +- Docker image names and versions. +- Data volume paths. +- Environment variables and secrets handling. +- Service dependencies. +- Restart policies. +- Health checks. +- Backup coverage for each service. +- Restore process for each service. +- Whether Joplin Server should be documented as part of the current homelab, as a VPS service, or both. diff --git a/saturn/ollama_client.py b/saturn/ollama_client.py new file mode 100644 index 0000000..f7fcf34 --- /dev/null +++ b/saturn/ollama_client.py @@ -0,0 +1,39 @@ +import json +import os +import urllib.error +import urllib.request + +OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434").rstrip("/") + + +def ask(prompt: str) -> str: + payload = { + "model": "deepseek-coder", + "messages": [{"role": "user", "content": prompt}], + "stream": False, + } + req = urllib.request.Request( + f"{OLLAMA_URL}/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/saturn/start-aider.sh b/saturn/start-aider.sh new file mode 100755 index 0000000..42fc1fc --- /dev/null +++ b/saturn/start-aider.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_FILE="$SCRIPT_DIR/codex_context.yaml" +DEFAULT_OLLAMA_API_BASE="http://100.100.231.104:11434" +AIDER_BIN="${AIDER_BIN:-$HOME/.local/bin/aider}" + +usage() { + cat <<'EOF' +Usage: ./start-aider.sh [aider-args...] + +Starts Aider from this repository with shared project context. +EOF +} + +if (($# > 0)); then + case "$1" in + -h|--help) + usage + exit 0 + ;; + esac +fi + +export OLLAMA_API_BASE="${OLLAMA_API_BASE:-$DEFAULT_OLLAMA_API_BASE}" + +cd "$SCRIPT_DIR" +printf 'Loading %s\n' "$STATE_FILE" + +if [[ ! -x "$AIDER_BIN" ]]; then + echo "Aider executable not found at $AIDER_BIN" >&2 + exit 1 +fi + +if "$AIDER_BIN" --help 2>/dev/null | grep -q -- '--read FILE'; then + printf '%s\n' 'Starting Aider with codex_context.yaml attached as read-only shared context.' + exec "$AIDER_BIN" \ + --model ollama/deepseek-coder:latest \ + --read "$STATE_FILE" \ + "$@" +fi + +printf '%s\n' 'Aider does not support read-only context files in this environment.' +printf '%s\n' 'Ask Aider to read codex_context.yaml first, then continue with your task.' +exec "$AIDER_BIN" --model qwen2.5-coder:14b"$@" diff --git a/saturn/start-codex.sh b/saturn/start-codex.sh new file mode 100755 index 0000000..3188bc6 --- /dev/null +++ b/saturn/start-codex.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_FILE="$SCRIPT_DIR/codex_context.yaml" +INITIAL_INSTRUCTION="Before doing any task, read codex_context.yaml and treat it as shared project memory." +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' "$INITIAL_INSTRUCTION" + 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 + + cd "$SCRIPT_DIR" + printf 'Loading %s\n' "$STATE_FILE" + exec codex --cd "$SCRIPT_DIR" "${codex_args[@]}" "$prompt" +} + +main "$@" diff --git a/saturn/update-context.md b/saturn/update-context.md new file mode 100644 index 0000000..03838fd --- /dev/null +++ b/saturn/update-context.md @@ -0,0 +1,8 @@ +# Shared Context Protocol + +- Shared memory file: `codex_context.yaml` +- Codex and Aider must both read `codex_context.yaml` before starting work. +- After any meaningful work, update `codex_context.yaml`. +- Record decisions, todos, issues, and host-specific state. +- Keep the file compressed, structured, and lossless. +- Do not store secrets in `codex_context.yaml`.