From f754a217f4162f30c66e95917a4bfe9621ffeba9 Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Wed, 1 Apr 2026 05:12:11 +0000 Subject: [PATCH] prepare runtime loop Co-authored-by: Codex --- .env.agent.example | 24 ++--- README.md | 16 +++- control.md | 6 ++ docs/agents/README.md | 31 +++++++ docs/agents/control-plane-agent.md | 56 ++++++++++++ docs/agents/node-agent.md | 53 +++++++++++ docs/agents/runtime-agent.md | 49 ++++++++++ scripts/agent-bootstrap | 5 +- scripts/dev-up | 4 +- scripts/lib/agent-env.sh | 139 +++++++++++++++++++++++++++++ scripts/lib/runtime-env.sh | 21 +++-- scripts/setup-clones | 26 +----- 12 files changed, 386 insertions(+), 44 deletions(-) create mode 100644 docs/agents/README.md create mode 100644 docs/agents/control-plane-agent.md create mode 100644 docs/agents/node-agent.md create mode 100644 docs/agents/runtime-agent.md create mode 100644 scripts/lib/agent-env.sh diff --git a/.env.agent.example b/.env.agent.example index b00a369..aa4ca07 100644 --- a/.env.agent.example +++ b/.env.agent.example @@ -1,12 +1,14 @@ -BETTERNAS_CLONE_NAME=betternas-main -COMPOSE_PROJECT_NAME=betternas-main -BETTERNAS_CONTROL_PLANE_PORT=3001 -BETTERNAS_NODE_AGENT_PORT=3090 -BETTERNAS_NEXTCLOUD_PORT=8080 -BETTERNAS_EXPORT_PATH=.state/betternas-main/export +# Run `pnpm agent:bootstrap` to generate a clone-local `.env.agent`. +# Copy this file to `.env.agent` only when you need to override specific values. +BETTERNAS_CLONE_NAME= +COMPOSE_PROJECT_NAME= +BETTERNAS_CONTROL_PLANE_PORT= +BETTERNAS_NODE_AGENT_PORT= +BETTERNAS_NEXTCLOUD_PORT= +BETTERNAS_EXPORT_PATH= BETTERNAS_VERSION=local-dev -BETTERNAS_NODE_DIRECT_ADDRESS=http://localhost:${BETTERNAS_NODE_AGENT_PORT} -BETTERNAS_EXAMPLE_MOUNT_URL=http://localhost:${BETTERNAS_NODE_AGENT_PORT}/dav/ -NEXTCLOUD_BASE_URL=http://localhost:${BETTERNAS_NEXTCLOUD_PORT} -NEXTCLOUD_ADMIN_USER=admin -NEXTCLOUD_ADMIN_PASSWORD=admin +BETTERNAS_NODE_DIRECT_ADDRESS= +BETTERNAS_EXAMPLE_MOUNT_URL= +NEXTCLOUD_BASE_URL= +NEXTCLOUD_ADMIN_USER= +NEXTCLOUD_ADMIN_PASSWORD= diff --git a/README.md b/README.md index b3fb072..e7ea9df 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,26 @@ Run the repo acceptance loop with: pnpm verify ``` -## Agent loop +## Runtime loop -Bootstrap a clone-local environment with: +Bootstrap clone-local runtime settings with: ```bash pnpm agent:bootstrap ``` +If `.env.agent` is missing, bootstrap writes clone-local defaults for this checkout. + +Bring the stack up, verify it, and tear it down with: + +```bash +pnpm stack:up +pnpm stack:verify +pnpm stack:down --volumes +``` + +## Agent loop + Run the full static and integration loop with: ```bash diff --git a/control.md b/control.md index 72e9a08..6aa58f2 100644 --- a/control.md +++ b/control.md @@ -43,3 +43,9 @@ Agent command surface: - main repo creates or refreshes sibling clones with `pnpm clones:setup` - each clone bootstraps itself with `pnpm agent:bootstrap` - each clone runs the full loop with `pnpm agent:verify` + +Agent prompts live in: + +- `docs/agents/runtime-agent.md` +- `docs/agents/control-plane-agent.md` +- `docs/agents/node-agent.md` diff --git a/docs/agents/README.md b/docs/agents/README.md new file mode 100644 index 0000000..d6dc8d6 --- /dev/null +++ b/docs/agents/README.md @@ -0,0 +1,31 @@ +# Agent Prompts + +These prompts are for the three sibling Devin clones: + +```text +/home/rathi/Documents/GitHub/betterNAS/ + betterNAS + betterNAS-runtime + betterNAS-control + betterNAS-node +``` + +Use them in this order: + +1. start the runtime agent first +2. wait until the runtime loop is green +3. start the control and node agents in parallel + +Rules that apply to all three: + +- `packages/contracts/**` is frozen for this wave +- `docs/architecture.md` is frozen for this wave +- if an agent finds a real contract gap, it should stop and report the exact change instead of freelancing a workaround +- each agent should stay inside its assigned lane unless a tiny unblocker is strictly required +- each agent must verify with real commands, not only code inspection + +Prompt files: + +- [`runtime-agent.md`](./runtime-agent.md) +- [`control-plane-agent.md`](./control-plane-agent.md) +- [`node-agent.md`](./node-agent.md) diff --git a/docs/agents/control-plane-agent.md b/docs/agents/control-plane-agent.md new file mode 100644 index 0000000..7584a57 --- /dev/null +++ b/docs/agents/control-plane-agent.md @@ -0,0 +1,56 @@ +# Control Plane Agent Prompt + +```text +You are working in /home/rathi/Documents/GitHub/betterNAS/betterNAS-control. + +Goal: +Make the Go control plane implement the current first-loop contracts for node registration, export inventory, heartbeat, mount profile issuance, and cloud profile issuance. + +Primary scope: +- apps/control-plane/** + +Read-only references: +- packages/contracts/** +- docs/architecture.md +- control.md +- TODO.md + +Do not change: +- packages/contracts/** +- docs/architecture.md +- runtime scripts +- node-agent code + +Use the existing contracts as fixed for this task. + +Implement cleanly: +- POST /api/v1/nodes/register + - store or update a node + - store or update its exports +- POST /api/v1/nodes/{nodeId}/heartbeat + - update status and lastSeenAt +- GET /api/v1/exports + - return registered exports +- POST /api/v1/mount-profiles/issue + - validate that the export exists + - return a mount profile for that export +- POST /api/v1/cloud-profiles/issue + - validate that the export exists + - return a Nextcloud cloud profile for that export + +Constraints: +- simplest correct implementation first +- in-memory storage is acceptable for this slice +- add tests +- do not invent new request or response shapes +- if you discover a real contract gap, stop and report the exact required contract change instead of patching around it + +Acceptance criteria: +1. pnpm agent:bootstrap succeeds. +2. pnpm verify succeeds. +3. control-plane tests cover the implemented API behavior. +4. If the runtime loop is already green on this machine, pnpm stack:up and pnpm stack:verify also stays green in this clone. + +Deliverable: +A real contract-backed control plane for the first mount loop, without contract drift. +``` diff --git a/docs/agents/node-agent.md b/docs/agents/node-agent.md new file mode 100644 index 0000000..a9d4d3e --- /dev/null +++ b/docs/agents/node-agent.md @@ -0,0 +1,53 @@ +# Node Agent Prompt + +```text +You are working in /home/rathi/Documents/GitHub/betterNAS/betterNAS-node. + +Goal: +Make the node agent a clean NAS-side runtime for the first loop, with reliable WebDAV behavior and export configuration. + +Primary scope: +- apps/node-agent/** + +Read-only references: +- packages/contracts/** +- docs/architecture.md +- control.md +- TODO.md + +Do not change: +- packages/contracts/** +- docs/architecture.md +- runtime scripts +- control-plane code + +Use the existing contracts as fixed for this task. + +Implement cleanly: +- stable WebDAV serving from BETTERNAS_EXPORT_PATH +- Finder-friendly behavior at /dav/ +- clean env-driven node identity and export metadata + - machine id + - display name + - export label + - tags if useful +- keep the health endpoint clean +- add tests where practical + +Optional only if it fits cleanly without changing contracts: +- node self-registration and heartbeat client wiring behind env configuration + +Constraints: +- do not invent new shared APIs +- keep this a boring, reliable NAS-side service +- prefer correctness and configurability over features + +Acceptance criteria: +1. pnpm agent:bootstrap succeeds. +2. pnpm verify succeeds. +3. WebDAV behavior is reliable against the configured export path. +4. If the runtime loop is already green on this machine, pnpm stack:up and pnpm stack:verify also stays green in this clone. + +Deliverable: +A clean node agent that serves the first real WebDAV export path without cross-lane drift. +``` diff --git a/docs/agents/runtime-agent.md b/docs/agents/runtime-agent.md new file mode 100644 index 0000000..9b6d810 --- /dev/null +++ b/docs/agents/runtime-agent.md @@ -0,0 +1,49 @@ +# Runtime Agent Prompt + +```text +You are working in /home/rathi/Documents/GitHub/betterNAS/betterNAS-runtime. + +Goal: +Make the clone-local runtime and integration loop deterministic and green on this machine. + +Primary scope: +- infra/docker/** +- scripts/** +- README.md +- control.md only if the command surface changes + +Do not change: +- packages/contracts/** +- docs/architecture.md +- app behavior unless a tiny startup or health fix is strictly required to get the runtime green + +Rules: +- keep this clone isolated and clone-safe +- do not hardcode ports or paths outside .env.agent +- do not invent new contracts +- prefer fixing runtime wiring, readiness, healthchecks, compose config, and verification scripts + +Required command surface: +- pnpm agent:bootstrap +- pnpm verify +- pnpm stack:up +- pnpm stack:verify +- pnpm stack:down --volumes + +Acceptance criteria: +1. From a fresh clone, pnpm agent:bootstrap succeeds. +2. pnpm verify succeeds. +3. pnpm stack:up succeeds. +4. pnpm stack:verify succeeds. +5. pnpm stack:down --volumes succeeds. +6. After a full reset, stack:up and stack:verify succeeds again. +7. The runtime stays deterministic and clone-safe. + +If blocked: +- inspect the actual failing service logs +- make the smallest necessary fix +- keep fixes inside runtime-owned files unless a tiny startup fix is unavoidable + +Deliverable: +A green runtime loop for this clone on this machine. +``` diff --git a/scripts/agent-bootstrap b/scripts/agent-bootstrap index 67cb815..4e24251 100755 --- a/scripts/agent-bootstrap +++ b/scripts/agent-bootstrap @@ -4,10 +4,11 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" env_file="$repo_root/.env.agent" -example_env_file="$repo_root/.env.agent.example" if [[ ! -f "$env_file" ]]; then - cp "$example_env_file" "$env_file" + # shellcheck disable=SC1091 + source "$repo_root/scripts/lib/agent-env.sh" + betternas_write_agent_env_file "$env_file" "$repo_root" fi # shellcheck disable=SC1091 diff --git a/scripts/dev-up b/scripts/dev-up index 05d7940..1a86050 100755 --- a/scripts/dev-up +++ b/scripts/dev-up @@ -51,8 +51,8 @@ if ! nextcloud_is_installed; then --database-host db \ --database-user nextcloud \ --database-pass nextcloud \ - --admin-user admin \ - --admin-pass admin \ + --admin-user "$NEXTCLOUD_ADMIN_USER" \ + --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ --data-dir /var/www/html/data if ! nextcloud_is_installed; then diff --git a/scripts/lib/agent-env.sh b/scripts/lib/agent-env.sh new file mode 100644 index 0000000..db23421 --- /dev/null +++ b/scripts/lib/agent-env.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash + +betternas_default_clone_name() { + local repo_path="${1-}" + local repo_name + + repo_name="$(basename "$repo_path")" + + case "$repo_name" in + betterNAS) + printf '%s' "betternas-main" + ;; + *) + printf '%s' "$repo_name" + ;; + esac +} + +betternas_default_compose_project_seed() { + local repo_path="${1-}" + local clone_name="${2-}" + + if [[ -n "$clone_name" ]]; then + printf '%s' "$clone_name" + return 0 + fi + + betternas_default_clone_name "$repo_path" +} + +betternas_normalize_compose_project_name() { + local raw_value="${1-}" + local value + + value="${raw_value,,}" + value="$( + printf '%s' "$value" | sed -E \ + -e 's/[^a-z0-9_-]+/-/g' \ + -e 's/^[^a-z0-9]+//' \ + -e 's/[-_]+$//' \ + -e 's/-+/-/g' + )" + + if [[ -z "$value" ]]; then + printf 'Unable to derive a valid Docker Compose project name from %q.\n' "$raw_value" >&2 + return 1 + fi + + printf '%s' "$value" +} + +betternas_resolve_compose_project_name() { + local repo_path="${1-}" + local override="${2-}" + local clone_name="${3-}" + local seed + + if [[ -n "$override" ]]; then + seed="$override" + else + seed="$(betternas_default_compose_project_seed "$repo_path" "$clone_name")" + fi + + betternas_normalize_compose_project_name "$seed" +} + +betternas_default_ports() { + local repo_path="${1-}" + local clone_name="${2-}" + local hash + local slot + local base + + if [[ -z "$clone_name" ]]; then + clone_name="$(betternas_default_clone_name "$repo_path")" + fi + + case "$clone_name" in + betternas-main) + printf '%s %s %s\n' "8080" "3090" "3001" + return 0 + ;; + betterNAS-runtime) + printf '%s %s %s\n' "41080" "41090" "41001" + return 0 + ;; + betterNAS-control) + printf '%s %s %s\n' "42080" "42090" "42001" + return 0 + ;; + betterNAS-node) + printf '%s %s %s\n' "43080" "43090" "43001" + return 0 + ;; + esac + + hash="$(printf '%s' "$repo_path" | cksum | awk '{print $1}')" + slot=$((hash % 1000)) + base=$((45000 + slot * 20)) + + printf '%s %s %s\n' "$base" "$((base + 10))" "$((base + 1))" +} + +betternas_write_env_assignment() { + local key="${1-}" + local value="${2-}" + + printf '%s=%q\n' "$key" "$value" +} + +betternas_write_agent_env_file() { + local env_path="${1-}" + local repo_path="${2-}" + local clone_name + local compose_project_name + local nextcloud_port + local node_agent_port + local control_plane_port + + clone_name="$(betternas_default_clone_name "$repo_path")" + compose_project_name="$(betternas_resolve_compose_project_name "$repo_path" "" "$clone_name")" + read -r nextcloud_port node_agent_port control_plane_port <<<"$(betternas_default_ports "$repo_path" "$clone_name")" + + { + echo "# Generated by pnpm agent:bootstrap" + betternas_write_env_assignment "BETTERNAS_CLONE_NAME" "$clone_name" + betternas_write_env_assignment "COMPOSE_PROJECT_NAME" "$compose_project_name" + betternas_write_env_assignment "BETTERNAS_CONTROL_PLANE_PORT" "$control_plane_port" + betternas_write_env_assignment "BETTERNAS_NODE_AGENT_PORT" "$node_agent_port" + betternas_write_env_assignment "BETTERNAS_NEXTCLOUD_PORT" "$nextcloud_port" + betternas_write_env_assignment "BETTERNAS_EXPORT_PATH" ".state/${clone_name}/export" + betternas_write_env_assignment "BETTERNAS_VERSION" "local-dev" + betternas_write_env_assignment "BETTERNAS_NODE_DIRECT_ADDRESS" "http://localhost:${node_agent_port}" + betternas_write_env_assignment "BETTERNAS_EXAMPLE_MOUNT_URL" "http://localhost:${node_agent_port}/dav/" + betternas_write_env_assignment "NEXTCLOUD_BASE_URL" "http://localhost:${nextcloud_port}" + betternas_write_env_assignment "NEXTCLOUD_ADMIN_USER" "admin" + betternas_write_env_assignment "NEXTCLOUD_ADMIN_PASSWORD" "admin" + } >"$env_path" +} diff --git a/scripts/lib/runtime-env.sh b/scripts/lib/runtime-env.sh index 9b6284b..45bc8ee 100755 --- a/scripts/lib/runtime-env.sh +++ b/scripts/lib/runtime-env.sh @@ -7,6 +7,9 @@ compose_file="$repo_root/infra/docker/compose.dev.yml" default_env_file="$repo_root/.env.agent" env_file="${BETTERNAS_ENV_FILE:-$default_env_file}" +# shellcheck disable=SC1091 +source "$repo_root/scripts/lib/agent-env.sh" + if [[ -f "$env_file" ]]; then set -a # shellcheck disable=SC1090 @@ -14,11 +17,19 @@ if [[ -f "$env_file" ]]; then set +a fi -: "${BETTERNAS_CLONE_NAME:=betternas-main}" -: "${COMPOSE_PROJECT_NAME:=betternas-${BETTERNAS_CLONE_NAME}}" -: "${BETTERNAS_CONTROL_PLANE_PORT:=3001}" -: "${BETTERNAS_NODE_AGENT_PORT:=3090}" -: "${BETTERNAS_NEXTCLOUD_PORT:=8080}" +if [[ -z "${BETTERNAS_CLONE_NAME:-}" ]]; then + BETTERNAS_CLONE_NAME="$(betternas_default_clone_name "$repo_root")" +fi + +COMPOSE_PROJECT_NAME="$( + betternas_resolve_compose_project_name "$repo_root" "${COMPOSE_PROJECT_NAME:-}" "$BETTERNAS_CLONE_NAME" +)" + +read -r default_nextcloud_port default_node_agent_port default_control_plane_port <<<"$(betternas_default_ports "$repo_root" "$BETTERNAS_CLONE_NAME")" + +: "${BETTERNAS_CONTROL_PLANE_PORT:=$default_control_plane_port}" +: "${BETTERNAS_NODE_AGENT_PORT:=$default_node_agent_port}" +: "${BETTERNAS_NEXTCLOUD_PORT:=$default_nextcloud_port}" : "${BETTERNAS_VERSION:=local-dev}" : "${NEXTCLOUD_ADMIN_USER:=admin}" : "${NEXTCLOUD_ADMIN_PASSWORD:=admin}" diff --git a/scripts/setup-clones b/scripts/setup-clones index 7b04e76..9432f31 100755 --- a/scripts/setup-clones +++ b/scripts/setup-clones @@ -7,34 +7,16 @@ parent_dir="$(cd "$repo_root/.." && pwd)" repo_name="$(basename "$repo_root")" sync_clone_script="$repo_root/scripts/sync-clone" -declare -A clone_ports=( - ["betterNAS-runtime"]="41080 41090 41001" - ["betterNAS-control"]="42080 42090 42001" - ["betterNAS-node"]="43080 43090 43001" -) +# shellcheck disable=SC1091 +source "$repo_root/scripts/lib/agent-env.sh" + clone_names=("betterNAS-runtime" "betterNAS-control" "betterNAS-node") for clone_name in "${clone_names[@]}"; do clone_dir="$parent_dir/$clone_name" "$sync_clone_script" "$clone_dir" - - read -r nextcloud_port node_agent_port control_plane_port <<<"${clone_ports[$clone_name]}" - - cat >"$clone_dir/.env.agent" <