nomarmy 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.md +484 -0
- package/bin/nomarmy.mjs +2248 -0
- package/config/agents.yml.example +63 -0
- package/config/common.env +31 -0
- package/config/profiles/bedrock-cheap.env +26 -0
- package/config/profiles/bedrock.env +28 -0
- package/config/profiles/cpu-linux.env +8 -0
- package/config/profiles/dgx-spark.env +12 -0
- package/config/profiles/macbook-pro.env +9 -0
- package/config/profiles/nvidia-linux.env +9 -0
- package/docker/Dockerfile +15 -0
- package/docker/Dockerfile.go +29 -0
- package/docker/Dockerfile.rust +19 -0
- package/e2e.sh +153 -0
- package/install.sh +125 -0
- package/lib/agents.mjs +285 -0
- package/lib/army.mjs +400 -0
- package/lib/budget.mjs +368 -0
- package/lib/claude-transcript.mjs +150 -0
- package/lib/config.mjs +193 -0
- package/lib/connect.mjs +409 -0
- package/lib/coordinator-instructions.mjs +23 -0
- package/lib/decompose.mjs +389 -0
- package/lib/dispatch-config.mjs +164 -0
- package/lib/dispatch-schema.mjs +280 -0
- package/lib/doctor.mjs +443 -0
- package/lib/evidence.mjs +679 -0
- package/lib/gguf.mjs +589 -0
- package/lib/hardware.mjs +476 -0
- package/lib/health.mjs +278 -0
- package/lib/model-catalog.mjs +71 -0
- package/lib/notifier-app.mjs +95 -0
- package/lib/notify.mjs +66 -0
- package/lib/openclaw-config.mjs +65 -0
- package/lib/openclaw-errors.mjs +40 -0
- package/lib/propose.mjs +110 -0
- package/lib/prune.mjs +77 -0
- package/lib/repo-query.mjs +267 -0
- package/lib/runs.mjs +150 -0
- package/lib/sabotage.mjs +128 -0
- package/lib/sandbox-images.mjs +434 -0
- package/lib/scan.mjs +1538 -0
- package/lib/schema.mjs +288 -0
- package/lib/scout.mjs +544 -0
- package/lib/sizing.mjs +1322 -0
- package/lib/slots.mjs +112 -0
- package/lib/statusline.mjs +126 -0
- package/lib/subscription-config.mjs +68 -0
- package/lib/subscription-setup.mjs +217 -0
- package/lib/transcript.mjs +195 -0
- package/lib/verify.mjs +700 -0
- package/mcp/server.mjs +4206 -0
- package/notifier/icon.swift +34 -0
- package/notifier/main.swift +52 -0
- package/notifier/nomarmy-icon.png +0 -0
- package/package.json +67 -0
- package/playbooks/feature.md +43 -0
- package/policies/coder.md +49 -0
- package/policies/orchestrator.md +35 -0
- package/policies/reviewer.md +35 -0
- package/policies/scout.md +65 -0
- package/scripts/configure-openclaw.sh +96 -0
- package/scripts/configure-orchestrator.sh +84 -0
- package/scripts/install-llama-cpp.sh +16 -0
- package/scripts/lib.sh +198 -0
- package/scripts/select-model.mjs +96 -0
- package/scripts/select-model.sh +4 -0
- package/scripts/setup-sandbox.sh +38 -0
- package/scripts/start-inference.sh +46 -0
- package/scripts/stop-inference.sh +5 -0
- package/scripts/uninstall.sh +6 -0
- package/scripts/verify-install.sh +68 -0
package/scripts/lib.sh
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
nomarmy_root(){ cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd; }
|
|
4
|
+
|
|
5
|
+
# v1.3 renamed the configuration namespace RAYSON_* -> NOMARMY_*. Anything a
|
|
6
|
+
# user or unit file still exports under the old prefix is translated once, so
|
|
7
|
+
# existing installs keep working while they migrate.
|
|
8
|
+
nomarmy_compat_prefix(){
|
|
9
|
+
local old new warned=0
|
|
10
|
+
for old in $(compgen -v | grep '^RAYSON_' || true); do
|
|
11
|
+
new="NOMARMY_${old#RAYSON_}"
|
|
12
|
+
[[ -n "${!new:-}" ]] && continue
|
|
13
|
+
export "$new=${!old}"
|
|
14
|
+
if [[ "$warned" -eq 0 ]]; then
|
|
15
|
+
echo "WARN: RAYSON_* environment variables are deprecated; use NOMARMY_* instead." >&2
|
|
16
|
+
warned=1
|
|
17
|
+
fi
|
|
18
|
+
echo "WARN: $old -> $new" >&2
|
|
19
|
+
done
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
nomarmy_available_profiles(){
|
|
23
|
+
local root; root="$(nomarmy_root)"
|
|
24
|
+
find "$root/config/profiles" -name '*.env' -exec basename {} .env \; | sort | tr '\n' ' '
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# True when the active profile runs inference on a hosted endpoint rather than
|
|
28
|
+
# a local llama-server. Scripts branch on this instead of on the profile name.
|
|
29
|
+
nomarmy_is_cloud(){ [[ "${NOMARMY_EXECUTION:-local}" != "local" ]]; }
|
|
30
|
+
|
|
31
|
+
nomarmy_validate_cloud(){
|
|
32
|
+
local missing=()
|
|
33
|
+
[[ -n "${NOMARMY_BEDROCK_REGION:-}" ]] || missing+=(NOMARMY_BEDROCK_REGION)
|
|
34
|
+
[[ -n "${NOMARMY_WORKER_MODEL:-}" ]] || missing+=(NOMARMY_WORKER_MODEL)
|
|
35
|
+
[[ -n "${NOMARMY_WORKER_PROVIDER:-}" ]] || missing+=(NOMARMY_WORKER_PROVIDER)
|
|
36
|
+
if [[ "${#missing[@]}" -gt 0 ]]; then
|
|
37
|
+
echo "ERROR: profile '$NOMARMY_PROFILE' is a cloud profile but these are unset: ${missing[*]}" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
# Fail here rather than let a malformed region surface as an opaque 400 from
|
|
41
|
+
# Bedrock in the middle of a worker job.
|
|
42
|
+
if [[ ! "$NOMARMY_BEDROCK_REGION" =~ ^[a-z]{2}(-gov)?-[a-z]+-[0-9]$ ]]; then
|
|
43
|
+
echo "ERROR: NOMARMY_BEDROCK_REGION='$NOMARMY_BEDROCK_REGION' is not a valid AWS region name." >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
export NOMARMY_BEDROCK_BASE_URL="${NOMARMY_BEDROCK_BASE_URL:-https://bedrock-runtime.${NOMARMY_BEDROCK_REGION}.amazonaws.com/openai/v1}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Precedence is environment > profile > common.env. The config files assign
|
|
50
|
+
# unconditionally, so a `${VAR:-default}` in a profile cannot tell a real user
|
|
51
|
+
# override from a value common.env set moments earlier. Snapshot what the caller
|
|
52
|
+
# exported before sourcing anything, and re-apply it afterwards.
|
|
53
|
+
NOMARMY_ENV_OVERRIDES=()
|
|
54
|
+
nomarmy_snapshot_env(){
|
|
55
|
+
local v
|
|
56
|
+
NOMARMY_ENV_OVERRIDES=()
|
|
57
|
+
for v in $(compgen -v | grep '^NOMARMY_' || true); do
|
|
58
|
+
# NOMARMY_PROFILE is resolved explicitly by load_profile; an explicit
|
|
59
|
+
# argument must beat an exported one, so it is not restored here.
|
|
60
|
+
[[ "$v" == "NOMARMY_PROFILE" || "$v" == "NOMARMY_ENV_OVERRIDES" ]] && continue
|
|
61
|
+
NOMARMY_ENV_OVERRIDES+=("$v=${!v}")
|
|
62
|
+
done
|
|
63
|
+
}
|
|
64
|
+
nomarmy_restore_env(){
|
|
65
|
+
local pair name value
|
|
66
|
+
for pair in ${NOMARMY_ENV_OVERRIDES[@]+"${NOMARMY_ENV_OVERRIDES[@]}"}; do
|
|
67
|
+
name="${pair%%=*}"; value="${pair#*=}"
|
|
68
|
+
export "$name=$value"
|
|
69
|
+
done
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# llama.cpp divides -c across -np, so per-nom context is the quotient. Surface
|
|
73
|
+
# the real figure and the two ways a profile can lie about its nom count.
|
|
74
|
+
nomarmy_validate_local(){
|
|
75
|
+
local parallel="${NOMARMY_LLAMA_PARALLEL:-1}" workers="${NOMARMY_MAX_WORKERS:-1}" ctx="${NOMARMY_LLAMA_CONTEXT:-0}" per_nom
|
|
76
|
+
[[ "$parallel" -ge 1 ]] || parallel=1
|
|
77
|
+
per_nom=$(( ctx / parallel ))
|
|
78
|
+
export NOMARMY_CONTEXT_PER_NOM="$per_nom"
|
|
79
|
+
|
|
80
|
+
if [[ "$workers" -gt "$parallel" ]]; then
|
|
81
|
+
echo "WARN: NOMARMY_MAX_WORKERS=$workers exceeds NOMARMY_LLAMA_PARALLEL=$parallel." >&2
|
|
82
|
+
echo "WARN: only $parallel nom(s) can infer at once; the rest queue for a slot while" >&2
|
|
83
|
+
echo "WARN: still holding a Podman sandbox each. Run 'nomarmy sizing' for a recommendation." >&2
|
|
84
|
+
fi
|
|
85
|
+
if [[ "$ctx" -gt 0 && "$per_nom" -lt 65536 ]]; then
|
|
86
|
+
if [[ "$parallel" -gt 1 ]]; then
|
|
87
|
+
# The surprising case: the operator set 65536 and the slot division ate it.
|
|
88
|
+
echo "WARN: context per nom is $per_nom ($ctx total / $parallel slots), below the" >&2
|
|
89
|
+
echo "WARN: v1.3 target of 65536. Raise NOMARMY_LLAMA_CONTEXT to $(( 65536 * parallel ))" >&2
|
|
90
|
+
echo "WARN: or reduce NOMARMY_LLAMA_PARALLEL. Run 'nomarmy sizing' for a recommendation." >&2
|
|
91
|
+
else
|
|
92
|
+
# Single slot: the operator chose this context directly, so just state it.
|
|
93
|
+
echo "INFO: context per nom is $per_nom; the v1.3 autonomous loop targets 65536." >&2
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
load_profile(){
|
|
99
|
+
local root profile profile_file os
|
|
100
|
+
nomarmy_compat_prefix
|
|
101
|
+
nomarmy_snapshot_env
|
|
102
|
+
root="$(nomarmy_root)"; profile="${1:-${NOMARMY_PROFILE:-}}"; os="$(uname -s)"
|
|
103
|
+
if [[ -z "$profile" ]]; then
|
|
104
|
+
if [[ "$os" == Darwin ]]; then profile=macbook-pro
|
|
105
|
+
elif command -v nvidia-smi >/dev/null 2>&1; then profile=nvidia-linux
|
|
106
|
+
else profile=cpu-linux; fi
|
|
107
|
+
fi
|
|
108
|
+
profile_file="$root/config/profiles/$profile.env"
|
|
109
|
+
if [[ ! -f "$profile_file" ]]; then
|
|
110
|
+
echo "ERROR: unknown profile '$profile'. Available: $(nomarmy_available_profiles)" >&2
|
|
111
|
+
exit 2
|
|
112
|
+
fi
|
|
113
|
+
set -a
|
|
114
|
+
# shellcheck disable=SC1091
|
|
115
|
+
source "$root/config/common.env"
|
|
116
|
+
# shellcheck disable=SC1090
|
|
117
|
+
source "$profile_file"
|
|
118
|
+
set +a
|
|
119
|
+
nomarmy_restore_env
|
|
120
|
+
export NOMARMY_PROFILE="$profile"
|
|
121
|
+
export NOMARMY_INSTALL_ROOT="${NOMARMY_INSTALL_ROOT/\$HOME/$HOME}"
|
|
122
|
+
|
|
123
|
+
if nomarmy_is_cloud; then
|
|
124
|
+
# Cloud profiles host no local model, so they carry no hardware or OS
|
|
125
|
+
# requirement and are the only profiles usable on a machine without a GPU.
|
|
126
|
+
nomarmy_validate_cloud
|
|
127
|
+
else
|
|
128
|
+
if [[ "$os" == Darwin && "$profile" != macbook-pro ]]; then
|
|
129
|
+
echo "ERROR: profile '$profile' requires Linux. Use macbook-pro on macOS." >&2
|
|
130
|
+
exit 2
|
|
131
|
+
fi
|
|
132
|
+
if [[ "$os" == Linux && "$profile" == macbook-pro ]]; then
|
|
133
|
+
echo "ERROR: profile 'macbook-pro' requires macOS. Use dgx-spark, nvidia-linux, or cpu-linux on Linux." >&2
|
|
134
|
+
exit 2
|
|
135
|
+
fi
|
|
136
|
+
nomarmy_validate_local
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if [[ "${NOMARMY_ORCHESTRATOR_TRUST:-frontier}" == "degraded" ]]; then
|
|
140
|
+
echo "WARN: profile '$profile' runs a DEGRADED orchestrator (${NOMARMY_ORCHESTRATOR_MODEL:-unset})." >&2
|
|
141
|
+
echo "WARN: coordinator and worker are the same capability class; acceptance is not an independent check." >&2
|
|
142
|
+
echo "WARN: see policies/reviewer.md before trusting this profile for anything costly to get wrong." >&2
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
need(){ command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing $1" >&2; return 1; }; }
|
|
147
|
+
|
|
148
|
+
run_as_root(){
|
|
149
|
+
if [[ "${EUID}" -eq 0 ]]; then "$@"; else need sudo; sudo "$@"; fi
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
linux_package_manager(){
|
|
153
|
+
if command -v apt-get >/dev/null 2>&1; then echo apt
|
|
154
|
+
elif command -v dnf >/dev/null 2>&1; then echo dnf
|
|
155
|
+
elif command -v yum >/dev/null 2>&1; then echo yum
|
|
156
|
+
elif command -v zypper >/dev/null 2>&1; then echo zypper
|
|
157
|
+
elif command -v pacman >/dev/null 2>&1; then echo pacman
|
|
158
|
+
elif command -v apk >/dev/null 2>&1; then echo apk
|
|
159
|
+
fi
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
install_build_dependencies(){
|
|
163
|
+
local manager; manager="$(linux_package_manager)"
|
|
164
|
+
if [[ -z "$manager" ]]; then
|
|
165
|
+
echo 'ERROR: unsupported Linux package manager. Install a C++ compiler, CMake, curl, Git, and CA certificates, then rerun.' >&2
|
|
166
|
+
return 1
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
echo "==> Installing build prerequisites with $manager"
|
|
170
|
+
case "$manager" in
|
|
171
|
+
apt) run_as_root apt-get update; run_as_root apt-get install -y build-essential cmake curl git ca-certificates nodejs npm ;;
|
|
172
|
+
dnf) run_as_root dnf install -y gcc-c++ cmake curl git ca-certificates nodejs npm ;;
|
|
173
|
+
yum) run_as_root yum install -y gcc-c++ cmake curl git ca-certificates nodejs npm ;;
|
|
174
|
+
zypper) run_as_root zypper --non-interactive install gcc-c++ cmake curl git ca-certificates nodejs npm ;;
|
|
175
|
+
pacman) run_as_root pacman --noconfirm --needed -S base-devel cmake curl git ca-certificates nodejs npm ;;
|
|
176
|
+
apk) run_as_root apk add build-base cmake curl git ca-certificates nodejs npm ;;
|
|
177
|
+
esac
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Podman is required on every Linux profile, cloud included -- unlike the
|
|
181
|
+
# build toolchain above, which cloud profiles skip entirely.
|
|
182
|
+
install_podman(){
|
|
183
|
+
local manager; manager="$(linux_package_manager)"
|
|
184
|
+
if [[ -z "$manager" ]]; then
|
|
185
|
+
echo 'ERROR: unsupported Linux package manager. Install Podman yourself, then rerun.' >&2
|
|
186
|
+
return 1
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
echo "==> Installing Podman with $manager"
|
|
190
|
+
case "$manager" in
|
|
191
|
+
apt) run_as_root apt-get update; run_as_root apt-get install -y podman ;;
|
|
192
|
+
dnf) run_as_root dnf install -y podman ;;
|
|
193
|
+
yum) run_as_root yum install -y podman ;;
|
|
194
|
+
zypper) run_as_root zypper --non-interactive install podman ;;
|
|
195
|
+
pacman) run_as_root pacman --noconfirm --needed -S podman ;;
|
|
196
|
+
apk) run_as_root apk add podman ;;
|
|
197
|
+
esac
|
|
198
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
let query = "";
|
|
10
|
+
let repo = "";
|
|
11
|
+
let configPath = path.join(root, "config", "common.env");
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
14
|
+
const value = args[index];
|
|
15
|
+
if (value === "--repo") repo = args[++index] ?? "";
|
|
16
|
+
else if (value === "--config") configPath = path.resolve(args[++index] ?? "");
|
|
17
|
+
else if (value === "--help" || value === "-h") {
|
|
18
|
+
console.log("Usage: ./scripts/select-model.sh <search term> | --repo <owner/model-GGUF> [--config <env file>]");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
} else if (!query) query = value;
|
|
21
|
+
else throw new Error(`Unexpected argument: ${value}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!repo && !query) throw new Error("Provide a model search term or --repo <owner/model-GGUF>.");
|
|
25
|
+
if (!process.stdin.isTTY) throw new Error("Model selection requires an interactive terminal so the configuration change can be confirmed.");
|
|
26
|
+
|
|
27
|
+
const rl = createInterface({ input, output });
|
|
28
|
+
const hf = async (endpoint) => {
|
|
29
|
+
const response = await fetch(`https://huggingface.co/api/${endpoint}`, {
|
|
30
|
+
headers: { "User-Agent": "nomArmy-model-selector/1.2" },
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) throw new Error(`Hugging Face request failed: ${response.status} ${response.statusText}`);
|
|
33
|
+
return response.json();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
if (!repo) {
|
|
38
|
+
const models = await hf(`models?search=${encodeURIComponent(query)}&filter=gguf&sort=downloads&direction=-1&limit=12&full=true`);
|
|
39
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
40
|
+
throw new Error(`No GGUF repositories matched "${query}". Try a more specific search or pass --repo owner/model-GGUF.`);
|
|
41
|
+
}
|
|
42
|
+
console.log(`\nGGUF repositories matching "${query}":`);
|
|
43
|
+
models.forEach((model, index) => console.log(` ${index + 1}. ${model.id} (${model.downloads ?? 0} downloads)`));
|
|
44
|
+
const choice = Number.parseInt(await rl.question("Choose a repository number (or press Enter to cancel): "), 10);
|
|
45
|
+
if (!Number.isInteger(choice) || choice < 1 || choice > models.length) throw new Error("Canceled; no model configuration changed.");
|
|
46
|
+
repo = models[choice - 1].id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) throw new Error("Repository must use the form owner/model.");
|
|
50
|
+
// encodeURIComponent(repo) as one call also escapes the real path-separating
|
|
51
|
+
// "/" between owner and name into "%2F" -- confirmed live: the HF API
|
|
52
|
+
// returns 400 for .../models/unsloth%2FMuse-Glimmer-30B-GGUF and 200 for
|
|
53
|
+
// .../models/unsloth/Muse-Glimmer-30B-GGUF. Encode each segment on its own
|
|
54
|
+
// and rejoin with a literal "/" so an owner or model name with its own
|
|
55
|
+
// special characters is still escaped correctly.
|
|
56
|
+
const details = await hf(`models/${repo.split("/").map(encodeURIComponent).join("/")}?blobs=true`);
|
|
57
|
+
const files = (details.siblings ?? []).map((file) => file.rfilename).filter((name) => /\.gguf$/i.test(name));
|
|
58
|
+
if (files.length === 0) throw new Error(`${repo} has no GGUF files. Choose a GGUF conversion repository for llama.cpp.`);
|
|
59
|
+
|
|
60
|
+
const preferred = ["Q4_K_M", "Q4_K_S", "Q4_0", "Q5_K_M", "Q5_K_S", "Q6_K", "Q8_0"];
|
|
61
|
+
const quants = [...new Set(files.map((name) => name.match(/(?:[._-])(IQ\d+_[A-Z0-9_]+|Q\d+_[A-Z0-9_]+)(?:[._-]|\.gguf$)/i)?.[1]?.toUpperCase()).filter(Boolean))];
|
|
62
|
+
quants.sort((left, right) => (preferred.indexOf(left) === -1 ? 99 : preferred.indexOf(left)) - (preferred.indexOf(right) === -1 ? 99 : preferred.indexOf(right)) || left.localeCompare(right));
|
|
63
|
+
const quant = quants[0] ?? "Q4_K_M";
|
|
64
|
+
console.log(`\nSelected repository: ${repo}`);
|
|
65
|
+
console.log(`Available quants: ${quants.length ? quants.join(", ") : "not detected; llama.cpp will choose its default"}`);
|
|
66
|
+
const requestedQuant = (await rl.question(`Quantization [${quant}]: `)).trim().toUpperCase() || quant;
|
|
67
|
+
if (!/^[A-Z0-9_]+$/.test(requestedQuant)) throw new Error("Quantization may contain only uppercase letters, numbers, and underscores.");
|
|
68
|
+
if (quants.length && !quants.includes(requestedQuant)) throw new Error(`Quantization ${requestedQuant} is not available in ${repo}.`);
|
|
69
|
+
const defaultAlias = repo.split("/").at(-1).replace(/-GGUF$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
70
|
+
const alias = (await rl.question(`Local model alias [${defaultAlias}]: `)).trim() || defaultAlias;
|
|
71
|
+
if (!/^[A-Za-z0-9._-]+$/.test(alias)) throw new Error("Alias may contain only letters, numbers, dots, underscores, and hyphens.");
|
|
72
|
+
|
|
73
|
+
console.log(`\nReady to update ${path.relative(root, configPath)}:`);
|
|
74
|
+
console.log(` NOMARMY_MODEL_REPO=${repo}`);
|
|
75
|
+
console.log(` NOMARMY_MODEL_QUANT=${requestedQuant}`);
|
|
76
|
+
console.log(` NOMARMY_MODEL_ALIAS=${alias}`);
|
|
77
|
+
if ((await rl.question("Apply this model configuration? [y/N] ")).trim().toLowerCase() !== "y") {
|
|
78
|
+
console.log("Canceled; no model configuration changed.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const existing = await readFile(configPath, "utf8");
|
|
83
|
+
const replace = (key, value, text) => {
|
|
84
|
+
const line = `${key}=${value}`;
|
|
85
|
+
return new RegExp(`^${key}=.*$`, "m").test(text) ? text.replace(new RegExp(`^${key}=.*$`, "m"), line) : `${text.trimEnd()}\n${line}\n`;
|
|
86
|
+
};
|
|
87
|
+
let updated = replace("NOMARMY_MODEL_REPO", repo, existing);
|
|
88
|
+
updated = replace("NOMARMY_MODEL_QUANT", requestedQuant, updated);
|
|
89
|
+
updated = replace("NOMARMY_MODEL_ALIAS", alias, updated);
|
|
90
|
+
await writeFile(configPath, updated);
|
|
91
|
+
console.log("Configuration updated. Restart inference to download and load the new model:");
|
|
92
|
+
console.log(" ./scripts/stop-inference.sh");
|
|
93
|
+
console.log(" ./scripts/start-inference.sh <profile>");
|
|
94
|
+
} finally {
|
|
95
|
+
rl.close();
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
IMAGE="${NOMARMY_AGENT_IMAGE:-openclaw-nomarmy-coder:bookworm}"
|
|
5
|
+
command -v podman >/dev/null || { echo "ERROR: podman not found"; exit 1; }
|
|
6
|
+
command -v openclaw >/dev/null || { echo "ERROR: openclaw not found"; exit 1; }
|
|
7
|
+
|
|
8
|
+
# Linux runs Podman rootless, directly on the host kernel -- no VM. macOS
|
|
9
|
+
# needs a lightweight VM (`podman machine`), the same category of thing as
|
|
10
|
+
# Docker Desktop but with a 2 GiB default ceiling instead of Docker Desktop's
|
|
11
|
+
# ~31 GiB, and no commercial licensing tier at any company size. Both calls
|
|
12
|
+
# are idempotent by design: an already-initialized/already-running machine
|
|
13
|
+
# just no-ops here, and `podman info` below is the real verification gate,
|
|
14
|
+
# not either exit code.
|
|
15
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
16
|
+
podman machine init 2>/dev/null || true
|
|
17
|
+
podman machine start 2>/dev/null || true
|
|
18
|
+
fi
|
|
19
|
+
podman info >/dev/null
|
|
20
|
+
|
|
21
|
+
echo "==> Building $IMAGE"
|
|
22
|
+
podman build -t "$IMAGE" -f "$ROOT/docker/Dockerfile" "$ROOT/docker"
|
|
23
|
+
echo "==> Configuring mandatory sandbox"
|
|
24
|
+
openclaw config set agents.defaults.sandbox.mode all
|
|
25
|
+
openclaw config set agents.defaults.sandbox.backend podman
|
|
26
|
+
openclaw config set agents.defaults.sandbox.scope session
|
|
27
|
+
openclaw config set agents.defaults.sandbox.workspaceAccess rw
|
|
28
|
+
# Still the "docker" config namespace even though the backend is podman --
|
|
29
|
+
# OpenClaw's schema nests both engines' image/network/readOnlyRoot settings
|
|
30
|
+
# under the same key.
|
|
31
|
+
openclaw config set agents.defaults.sandbox.docker.image "$IMAGE"
|
|
32
|
+
openclaw config set agents.defaults.sandbox.docker.network none
|
|
33
|
+
openclaw config set agents.defaults.sandbox.docker.readOnlyRoot true
|
|
34
|
+
openclaw config set tools.exec.host auto
|
|
35
|
+
openclaw config set tools.elevated.enabled false || true
|
|
36
|
+
openclaw sandbox recreate --all || true
|
|
37
|
+
echo "==> Sandbox configured"
|
|
38
|
+
openclaw config get agents.defaults.sandbox
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; source "$ROOT/scripts/lib.sh"; load_profile "${1:-}"
|
|
4
|
+
if nomarmy_is_cloud; then echo "Profile '$NOMARMY_PROFILE' uses hosted inference at $NOMARMY_BEDROCK_BASE_URL; no local llama-server to start."; exit 0; fi
|
|
5
|
+
mkdir -p "$NOMARMY_INSTALL_ROOT/run" "$NOMARMY_INSTALL_ROOT/logs"
|
|
6
|
+
PID="$NOMARMY_INSTALL_ROOT/run/llama.pid"; LOG="$NOMARMY_INSTALL_ROOT/logs/llama-server.log"
|
|
7
|
+
if [[ -f "$PID" ]] && kill -0 "$(cat "$PID")" 2>/dev/null; then echo "llama-server already running PID $(cat "$PID")"; exit 0; fi
|
|
8
|
+
BIN="$NOMARMY_INSTALL_ROOT/llama-server"; [[ -x "$BIN" ]] || { echo "ERROR: $BIN missing"; exit 1; }
|
|
9
|
+
ARGS=(-hf "$NOMARMY_MODEL_REPO:$NOMARMY_MODEL_QUANT" --alias "$NOMARMY_MODEL_ALIAS" --host "$NOMARMY_LLAMA_HOST" --port "$NOMARMY_LLAMA_PORT" -c "$NOMARMY_LLAMA_CONTEXT" -np "$NOMARMY_LLAMA_PARALLEL" -ngl "$NOMARMY_LLAMA_GPU_LAYERS" -t "$NOMARMY_LLAMA_THREADS" --metrics)
|
|
10
|
+
# Everything below is optional advanced tuning, unset by default: nothing
|
|
11
|
+
# here changes behavior for anyone who hasn't set these. See README's
|
|
12
|
+
# "Advanced llama-server tuning" section for what each one is for.
|
|
13
|
+
[[ -n "${NOMARMY_LLAMA_CACHE_TYPE_K:-}" ]] && ARGS+=(--cache-type-k "$NOMARMY_LLAMA_CACHE_TYPE_K")
|
|
14
|
+
[[ -n "${NOMARMY_LLAMA_CACHE_TYPE_V:-}" ]] && ARGS+=(--cache-type-v "$NOMARMY_LLAMA_CACHE_TYPE_V")
|
|
15
|
+
[[ -n "${NOMARMY_LLAMA_FLASH_ATTN:-}" ]] && ARGS+=(--flash-attn "$NOMARMY_LLAMA_FLASH_ATTN")
|
|
16
|
+
[[ -n "${NOMARMY_LLAMA_REASONING_BUDGET:-}" ]] && ARGS+=(--reasoning-budget "$NOMARMY_LLAMA_REASONING_BUDGET")
|
|
17
|
+
if [[ -n "${NOMARMY_LLAMA_REASONING_PRESERVE:-}" ]]; then
|
|
18
|
+
if [[ "$NOMARMY_LLAMA_REASONING_PRESERVE" == "true" ]]; then ARGS+=(--reasoning-preserve)
|
|
19
|
+
else ARGS+=(--no-reasoning-preserve)
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
# Raw escape hatch for any current or future llama-server flag this script
|
|
23
|
+
# does not name explicitly. Word-split intentionally; quote-embedded spaces
|
|
24
|
+
# are not supported, matching how every other *_EXTRA_ARGS-style env var works.
|
|
25
|
+
if [[ -n "${NOMARMY_LLAMA_EXTRA_ARGS:-}" ]]; then
|
|
26
|
+
# shellcheck disable=SC2206
|
|
27
|
+
EXTRA=($NOMARMY_LLAMA_EXTRA_ARGS)
|
|
28
|
+
ARGS+=("${EXTRA[@]}")
|
|
29
|
+
fi
|
|
30
|
+
nohup "$BIN" "${ARGS[@]}" >"$LOG" 2>&1 &
|
|
31
|
+
echo $! > "$PID"
|
|
32
|
+
echo "Started llama-server PID $(cat "$PID"); first model download/load may take time. Log: $LOG"
|
|
33
|
+
became_healthy=0
|
|
34
|
+
for i in $(seq 1 180); do curl -fsS "http://$NOMARMY_LLAMA_HOST:$NOMARMY_LLAMA_PORT/health" >/dev/null 2>&1 && { became_healthy=1; break; }; sleep 2; done
|
|
35
|
+
if [[ "$became_healthy" -ne 1 ]]; then
|
|
36
|
+
echo "ERROR: inference did not become healthy; tail $LOG"; tail -80 "$LOG"; exit 1
|
|
37
|
+
fi
|
|
38
|
+
# /health only reports that a model is loaded into slots, not that the backend
|
|
39
|
+
# can actually compute -- a Metal/CUDA allocation failure during load (seen in
|
|
40
|
+
# practice: a real "Insufficient Memory" GPU error, kIOGPUCommandBufferCallbackErrorOutOfMemory)
|
|
41
|
+
# still leaves /health reporting fine. Do one real forward pass before calling it healthy.
|
|
42
|
+
if curl -fsS -m 30 -X POST "http://$NOMARMY_LLAMA_HOST:$NOMARMY_LLAMA_PORT/completion" \
|
|
43
|
+
-H "Content-Type: application/json" -d '{"prompt":"ok","n_predict":1}' >/dev/null 2>&1; then
|
|
44
|
+
echo 'Inference healthy'; exit 0
|
|
45
|
+
fi
|
|
46
|
+
echo "ERROR: model loaded but a real completion request failed -- likely a GPU/compute allocation error, not a startup timing issue; tail $LOG"; tail -80 "$LOG"; exit 1
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; source "$ROOT/scripts/lib.sh"; load_profile "${1:-}"
|
|
4
|
+
if nomarmy_is_cloud; then echo "Profile '$NOMARMY_PROFILE' uses hosted inference; nothing local to stop."; exit 0; fi
|
|
5
|
+
PID="$NOMARMY_INSTALL_ROOT/run/llama.pid"; if [[ -f "$PID" ]] && kill -0 "$(cat "$PID")" 2>/dev/null; then kill "$(cat "$PID")"; fi; rm -f "$PID"; echo stopped
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
INSTALL_DIR="${NOMARMY_AGENT_INSTALL_DIR:-$HOME/.local/share/nomarmy-local-worker}"
|
|
4
|
+
claude mcp remove nomarmy-local-worker >/dev/null 2>&1 || true
|
|
5
|
+
rm -rf "$INSTALL_DIR"
|
|
6
|
+
echo "Removed nomarmy-local-worker MCP installation. Retained ~/.local/share/nomarmy-local-agents/jobs for recovery/audit."
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; source "$ROOT/scripts/lib.sh"; load_profile "${1:-}"
|
|
4
|
+
fail=0
|
|
5
|
+
check(){ if "$@" >/dev/null 2>&1; then echo "PASS $*"; else echo "FAIL $*"; fail=1; fi; }
|
|
6
|
+
|
|
7
|
+
check git --version; check podman info; check openclaw --version
|
|
8
|
+
|
|
9
|
+
if nomarmy_is_cloud; then
|
|
10
|
+
echo "INFO cloud profile '$NOMARMY_PROFILE' (region $NOMARMY_BEDROCK_REGION): no local inference expected"
|
|
11
|
+
check aws --version
|
|
12
|
+
if [[ -n "${NOMARMY_BEDROCK_API_KEY:-${AWS_BEARER_TOKEN_BEDROCK:-}}" ]]; then
|
|
13
|
+
echo 'PASS Bedrock API key present in environment'
|
|
14
|
+
else
|
|
15
|
+
check aws sts get-caller-identity --region "$NOMARMY_BEDROCK_REGION"
|
|
16
|
+
fi
|
|
17
|
+
# Catch a model ID that is wrong or not enabled in this account/region here,
|
|
18
|
+
# rather than as an opaque failure in the middle of a worker job.
|
|
19
|
+
for model in "$NOMARMY_WORKER_MODEL" "$NOMARMY_WORKER_MODEL_FALLBACK"; do
|
|
20
|
+
if aws bedrock list-foundation-models --region "$NOMARMY_BEDROCK_REGION" \
|
|
21
|
+
--query 'modelSummaries[].modelId' --output text 2>/dev/null | grep -qF "$model"; then
|
|
22
|
+
echo "PASS Bedrock model available: $model"
|
|
23
|
+
else
|
|
24
|
+
echo "FAIL Bedrock model not listed in $NOMARMY_BEDROCK_REGION: $model"; fail=1
|
|
25
|
+
fi
|
|
26
|
+
done
|
|
27
|
+
else
|
|
28
|
+
check "$NOMARMY_INSTALL_ROOT/llama-server" --version
|
|
29
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
30
|
+
system_profiler SPDisplaysDataType 2>/dev/null | grep -qi 'Metal' && echo 'PASS Apple Metal detected' || echo 'WARN Metal detection inconclusive'
|
|
31
|
+
elif [[ "$NOMARMY_PROFILE" == dgx-spark || "$NOMARMY_PROFILE" == nvidia-linux ]]; then
|
|
32
|
+
check nvidia-smi
|
|
33
|
+
else
|
|
34
|
+
echo 'PASS CPU-only Linux profile'
|
|
35
|
+
fi
|
|
36
|
+
check curl -fsS "http://$NOMARMY_LLAMA_HOST:$NOMARMY_LLAMA_PORT/health"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
openclaw models list --provider "$NOMARMY_WORKER_PROVIDER" | grep -q "$NOMARMY_WORKER_MODEL" \
|
|
40
|
+
&& echo "PASS OpenClaw worker model ($NOMARMY_WORKER_PROVIDER/$NOMARMY_WORKER_MODEL)" \
|
|
41
|
+
|| { echo "FAIL OpenClaw worker model ($NOMARMY_WORKER_PROVIDER/$NOMARMY_WORKER_MODEL)"; fail=1; }
|
|
42
|
+
# The sandbox config keeps its "docker" sub-key namespace regardless of
|
|
43
|
+
# backend, so grepping the whole block for "docker" would falsely pass even
|
|
44
|
+
# when the backend is podman. Check the actual backend value instead.
|
|
45
|
+
SANDBOX_BACKEND="$(openclaw config get agents.defaults.sandbox.backend 2>/dev/null | tr -d '[:space:]"' || true)"
|
|
46
|
+
[[ "$SANDBOX_BACKEND" == "podman" ]] && echo 'PASS Podman sandbox configured' || { echo "FAIL Podman sandbox (backend is '$SANDBOX_BACKEND')"; fail=1; }
|
|
47
|
+
|
|
48
|
+
# The no-network sandbox is what keeps repository content away from any
|
|
49
|
+
# credential the host process holds. It is a hard requirement on cloud profiles.
|
|
50
|
+
SANDBOX_NET="$(openclaw config get agents.defaults.sandbox.docker.network 2>/dev/null | tr -d '[:space:]"' || true)"
|
|
51
|
+
if [[ "$SANDBOX_NET" == "none" ]]; then
|
|
52
|
+
echo 'PASS Sandbox network isolated'
|
|
53
|
+
elif nomarmy_is_cloud; then
|
|
54
|
+
echo "FAIL Sandbox network is '$SANDBOX_NET', must be 'none' on a cloud profile"; fail=1
|
|
55
|
+
else
|
|
56
|
+
echo "WARN Sandbox network is '$SANDBOX_NET', expected 'none'"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
node --check "$ROOT/mcp/server.mjs" && echo 'PASS MCP syntax'
|
|
60
|
+
|
|
61
|
+
if [[ "${NOMARMY_ORCHESTRATOR_TRUST:-frontier}" == "degraded" ]]; then
|
|
62
|
+
echo "WARN Orchestrator trust is DEGRADED (${NOMARMY_ORCHESTRATOR_MODEL:-unset}): acceptance is not an independent check"
|
|
63
|
+
else
|
|
64
|
+
echo "PASS Orchestrator trust is frontier (${NOMARMY_ORCHESTRATOR_MODEL:-local coordinator})"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if command -v claude >/dev/null 2>&1; then claude mcp get nomarmy-local-worker >/dev/null 2>&1 && echo 'PASS Claude MCP registration' || echo 'WARN Claude installed but MCP not registered'; else echo 'INFO Claude Code not installed on this node'; fi
|
|
68
|
+
exit "$fail"
|