prepare runtime loop

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Harivansh Rathi 2026-04-01 05:12:11 +00:00
parent e75b3f98a6
commit f754a217f4
12 changed files with 386 additions and 44 deletions

View file

@ -1,12 +1,14 @@
BETTERNAS_CLONE_NAME=betternas-main # Run `pnpm agent:bootstrap` to generate a clone-local `.env.agent`.
COMPOSE_PROJECT_NAME=betternas-main # Copy this file to `.env.agent` only when you need to override specific values.
BETTERNAS_CONTROL_PLANE_PORT=3001 BETTERNAS_CLONE_NAME=
BETTERNAS_NODE_AGENT_PORT=3090 COMPOSE_PROJECT_NAME=
BETTERNAS_NEXTCLOUD_PORT=8080 BETTERNAS_CONTROL_PLANE_PORT=
BETTERNAS_EXPORT_PATH=.state/betternas-main/export BETTERNAS_NODE_AGENT_PORT=
BETTERNAS_NEXTCLOUD_PORT=
BETTERNAS_EXPORT_PATH=
BETTERNAS_VERSION=local-dev BETTERNAS_VERSION=local-dev
BETTERNAS_NODE_DIRECT_ADDRESS=http://localhost:${BETTERNAS_NODE_AGENT_PORT} BETTERNAS_NODE_DIRECT_ADDRESS=
BETTERNAS_EXAMPLE_MOUNT_URL=http://localhost:${BETTERNAS_NODE_AGENT_PORT}/dav/ BETTERNAS_EXAMPLE_MOUNT_URL=
NEXTCLOUD_BASE_URL=http://localhost:${BETTERNAS_NEXTCLOUD_PORT} NEXTCLOUD_BASE_URL=
NEXTCLOUD_ADMIN_USER=admin NEXTCLOUD_ADMIN_USER=
NEXTCLOUD_ADMIN_PASSWORD=admin NEXTCLOUD_ADMIN_PASSWORD=

View file

@ -25,14 +25,26 @@ Run the repo acceptance loop with:
pnpm verify pnpm verify
``` ```
## Agent loop ## Runtime loop
Bootstrap a clone-local environment with: Bootstrap clone-local runtime settings with:
```bash ```bash
pnpm agent:bootstrap 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: Run the full static and integration loop with:
```bash ```bash

View file

@ -43,3 +43,9 @@ Agent command surface:
- main repo creates or refreshes sibling clones with `pnpm clones:setup` - main repo creates or refreshes sibling clones with `pnpm clones:setup`
- each clone bootstraps itself with `pnpm agent:bootstrap` - each clone bootstraps itself with `pnpm agent:bootstrap`
- each clone runs the full loop with `pnpm agent:verify` - 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`

31
docs/agents/README.md Normal file
View file

@ -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)

View file

@ -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.
```

53
docs/agents/node-agent.md Normal file
View file

@ -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.
```

View file

@ -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.
```

View file

@ -4,10 +4,11 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
env_file="$repo_root/.env.agent" env_file="$repo_root/.env.agent"
example_env_file="$repo_root/.env.agent.example"
if [[ ! -f "$env_file" ]]; then 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 fi
# shellcheck disable=SC1091 # shellcheck disable=SC1091

View file

@ -51,8 +51,8 @@ if ! nextcloud_is_installed; then
--database-host db \ --database-host db \
--database-user nextcloud \ --database-user nextcloud \
--database-pass nextcloud \ --database-pass nextcloud \
--admin-user admin \ --admin-user "$NEXTCLOUD_ADMIN_USER" \
--admin-pass admin \ --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \
--data-dir /var/www/html/data --data-dir /var/www/html/data
if ! nextcloud_is_installed; then if ! nextcloud_is_installed; then

139
scripts/lib/agent-env.sh Normal file
View file

@ -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"
}

View file

@ -7,6 +7,9 @@ compose_file="$repo_root/infra/docker/compose.dev.yml"
default_env_file="$repo_root/.env.agent" default_env_file="$repo_root/.env.agent"
env_file="${BETTERNAS_ENV_FILE:-$default_env_file}" env_file="${BETTERNAS_ENV_FILE:-$default_env_file}"
# shellcheck disable=SC1091
source "$repo_root/scripts/lib/agent-env.sh"
if [[ -f "$env_file" ]]; then if [[ -f "$env_file" ]]; then
set -a set -a
# shellcheck disable=SC1090 # shellcheck disable=SC1090
@ -14,11 +17,19 @@ if [[ -f "$env_file" ]]; then
set +a set +a
fi fi
: "${BETTERNAS_CLONE_NAME:=betternas-main}" if [[ -z "${BETTERNAS_CLONE_NAME:-}" ]]; then
: "${COMPOSE_PROJECT_NAME:=betternas-${BETTERNAS_CLONE_NAME}}" BETTERNAS_CLONE_NAME="$(betternas_default_clone_name "$repo_root")"
: "${BETTERNAS_CONTROL_PLANE_PORT:=3001}" fi
: "${BETTERNAS_NODE_AGENT_PORT:=3090}"
: "${BETTERNAS_NEXTCLOUD_PORT:=8080}" 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}" : "${BETTERNAS_VERSION:=local-dev}"
: "${NEXTCLOUD_ADMIN_USER:=admin}" : "${NEXTCLOUD_ADMIN_USER:=admin}"
: "${NEXTCLOUD_ADMIN_PASSWORD:=admin}" : "${NEXTCLOUD_ADMIN_PASSWORD:=admin}"

View file

@ -7,34 +7,16 @@ parent_dir="$(cd "$repo_root/.." && pwd)"
repo_name="$(basename "$repo_root")" repo_name="$(basename "$repo_root")"
sync_clone_script="$repo_root/scripts/sync-clone" sync_clone_script="$repo_root/scripts/sync-clone"
declare -A clone_ports=( # shellcheck disable=SC1091
["betterNAS-runtime"]="41080 41090 41001" source "$repo_root/scripts/lib/agent-env.sh"
["betterNAS-control"]="42080 42090 42001"
["betterNAS-node"]="43080 43090 43001"
)
clone_names=("betterNAS-runtime" "betterNAS-control" "betterNAS-node") clone_names=("betterNAS-runtime" "betterNAS-control" "betterNAS-node")
for clone_name in "${clone_names[@]}"; do for clone_name in "${clone_names[@]}"; do
clone_dir="$parent_dir/$clone_name" clone_dir="$parent_dir/$clone_name"
"$sync_clone_script" "$clone_dir" "$sync_clone_script" "$clone_dir"
betternas_write_agent_env_file "$clone_dir/.env.agent" "$clone_dir"
read -r nextcloud_port node_agent_port control_plane_port <<<"${clone_ports[$clone_name]}"
cat >"$clone_dir/.env.agent" <<EOF
BETTERNAS_CLONE_NAME=${clone_name}
COMPOSE_PROJECT_NAME=${clone_name}
BETTERNAS_NEXTCLOUD_PORT=${nextcloud_port}
BETTERNAS_NODE_AGENT_PORT=${node_agent_port}
BETTERNAS_CONTROL_PLANE_PORT=${control_plane_port}
BETTERNAS_EXPORT_PATH=.state/${clone_name}/export
BETTERNAS_VERSION=local-dev
BETTERNAS_NODE_DIRECT_ADDRESS=http://localhost:${node_agent_port}
BETTERNAS_EXAMPLE_MOUNT_URL=http://localhost:${node_agent_port}/dav/
NEXTCLOUD_BASE_URL=http://localhost:${nextcloud_port}
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=admin
EOF
done done
cat <<EOF cat <<EOF