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.
Files changed (74) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +25 -0
  3. package/README.md +484 -0
  4. package/bin/nomarmy.mjs +2248 -0
  5. package/config/agents.yml.example +63 -0
  6. package/config/common.env +31 -0
  7. package/config/profiles/bedrock-cheap.env +26 -0
  8. package/config/profiles/bedrock.env +28 -0
  9. package/config/profiles/cpu-linux.env +8 -0
  10. package/config/profiles/dgx-spark.env +12 -0
  11. package/config/profiles/macbook-pro.env +9 -0
  12. package/config/profiles/nvidia-linux.env +9 -0
  13. package/docker/Dockerfile +15 -0
  14. package/docker/Dockerfile.go +29 -0
  15. package/docker/Dockerfile.rust +19 -0
  16. package/e2e.sh +153 -0
  17. package/install.sh +125 -0
  18. package/lib/agents.mjs +285 -0
  19. package/lib/army.mjs +400 -0
  20. package/lib/budget.mjs +368 -0
  21. package/lib/claude-transcript.mjs +150 -0
  22. package/lib/config.mjs +193 -0
  23. package/lib/connect.mjs +409 -0
  24. package/lib/coordinator-instructions.mjs +23 -0
  25. package/lib/decompose.mjs +389 -0
  26. package/lib/dispatch-config.mjs +164 -0
  27. package/lib/dispatch-schema.mjs +280 -0
  28. package/lib/doctor.mjs +443 -0
  29. package/lib/evidence.mjs +679 -0
  30. package/lib/gguf.mjs +589 -0
  31. package/lib/hardware.mjs +476 -0
  32. package/lib/health.mjs +278 -0
  33. package/lib/model-catalog.mjs +71 -0
  34. package/lib/notifier-app.mjs +95 -0
  35. package/lib/notify.mjs +66 -0
  36. package/lib/openclaw-config.mjs +65 -0
  37. package/lib/openclaw-errors.mjs +40 -0
  38. package/lib/propose.mjs +110 -0
  39. package/lib/prune.mjs +77 -0
  40. package/lib/repo-query.mjs +267 -0
  41. package/lib/runs.mjs +150 -0
  42. package/lib/sabotage.mjs +128 -0
  43. package/lib/sandbox-images.mjs +434 -0
  44. package/lib/scan.mjs +1538 -0
  45. package/lib/schema.mjs +288 -0
  46. package/lib/scout.mjs +544 -0
  47. package/lib/sizing.mjs +1322 -0
  48. package/lib/slots.mjs +112 -0
  49. package/lib/statusline.mjs +126 -0
  50. package/lib/subscription-config.mjs +68 -0
  51. package/lib/subscription-setup.mjs +217 -0
  52. package/lib/transcript.mjs +195 -0
  53. package/lib/verify.mjs +700 -0
  54. package/mcp/server.mjs +4206 -0
  55. package/notifier/icon.swift +34 -0
  56. package/notifier/main.swift +52 -0
  57. package/notifier/nomarmy-icon.png +0 -0
  58. package/package.json +67 -0
  59. package/playbooks/feature.md +43 -0
  60. package/policies/coder.md +49 -0
  61. package/policies/orchestrator.md +35 -0
  62. package/policies/reviewer.md +35 -0
  63. package/policies/scout.md +65 -0
  64. package/scripts/configure-openclaw.sh +96 -0
  65. package/scripts/configure-orchestrator.sh +84 -0
  66. package/scripts/install-llama-cpp.sh +16 -0
  67. package/scripts/lib.sh +198 -0
  68. package/scripts/select-model.mjs +96 -0
  69. package/scripts/select-model.sh +4 -0
  70. package/scripts/setup-sandbox.sh +38 -0
  71. package/scripts/start-inference.sh +46 -0
  72. package/scripts/stop-inference.sh +5 -0
  73. package/scripts/uninstall.sh +6 -0
  74. 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,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ exec node "$ROOT/scripts/select-model.mjs" "$@"
@@ -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"