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
@@ -0,0 +1,280 @@
1
+ // nomArmy api provider-entry schema (v1.3).
2
+ //
3
+ // This module owns the per-provider rules for an api agent: which provider
4
+ // types exist, which need base_url, how the generic `openclaw` type names
5
+ // its provider id. lib/agents.mjs validates every `kind: api` agent in
6
+ // agents.yml as one of these entries (a one-entry pool, the shape the
7
+ // dispatch path was written for). No filesystem access, no YAML.
8
+ //
9
+ // Design rules (same as lib/schema.mjs):
10
+ // * Every object is strict. An unrecognized field is a configuration
11
+ // error, never a silently ignored key.
12
+ // * A provider entry never carries a raw credential -- only `auth_env`,
13
+ // the NAME of an environment variable nomArmy reads at dispatch time.
14
+ // The credential itself lives in the operator's shell/secrets manager,
15
+ // never in a file nomArmy writes or reads back.
16
+
17
+ import { z } from "zod";
18
+
19
+ const requiredString = () =>
20
+ z
21
+ .string({ required_error: "is required", invalid_type_error: "must be a string" })
22
+ .refine((value) => value.trim().length > 0, { message: "must not be empty" });
23
+
24
+ // Exported (not just used internally) so bin/nomarmy.mjs's interactive
25
+ // `agents add` wizard can validate as-you-type and re-prompt immediately,
26
+ // instead of only failing via schema validation after every question has
27
+ // already been answered -- a real confusion this caused live: a user typed
28
+ // their actual API key into the "Entry id" prompt (which looks like the
29
+ // first free-text field after picking a provider) and only found out it was
30
+ // invalid at the very end, with no indication which answer was the problem.
31
+ export const ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
32
+ export const AUTH_ENV_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
33
+
34
+ /**
35
+ * `z.record()` silently drops a key literally named "__proto__" (verified
36
+ * live: no prototype pollution results, since a computed-key object
37
+ * literal is spec-safe, but the pool and every entry in it vanish with no
38
+ * validation error at all -- the opposite of this schema's own "unknown
39
+ * field is a hard error, never silently ignored" rule). Checked on the RAW,
40
+ * not-yet-validated object's own keys, because by the time any
41
+ * `.superRefine` could see `pools` the record step has already dropped it;
42
+ * callers run this before `dispatchConfigSchema.safeParse`.
43
+ */
44
+ export const RESERVED_POOL_NAMES = Object.freeze(["__proto__", "constructor", "prototype"]);
45
+ export function findReservedPoolName(candidate) {
46
+ const pools = candidate && typeof candidate === "object" ? candidate.pools : null;
47
+ if (!pools || typeof pools !== "object") return null;
48
+ return RESERVED_POOL_NAMES.find((name) => Object.prototype.hasOwnProperty.call(pools, name)) ?? null;
49
+ }
50
+
51
+ /**
52
+ * Provider types nomArmy knows how to register with OpenClaw (see
53
+ * scripts/configure-openclaw.sh). `llama-cpp` is the only one with no
54
+ * `auth_env` -- it shares the already-configured local server's
55
+ * credential-free setup. `anthropic`/`openai`/`xai`/`deepinfra` register
56
+ * through OpenClaw's own native onboarding flags (--anthropic-api-key, etc);
57
+ * `openclaw` is the generic form of that for any OTHER provider OpenClaw
58
+ * knows, or gets from a plugin (DeepSeek, Mistral, Groq, ...), named by
59
+ * `openclaw_provider` so nomArmy never has to enumerate them;
60
+ * `bedrock` and `azure-openai` register as a custom endpoint with an
61
+ * explicit base URL, the same mechanism this codebase already uses for
62
+ * Bedrock. `openai-compatible` is the escape hatch for anything else with an
63
+ * OpenAI-shaped endpoint and always needs its own `base_url`.
64
+ */
65
+ export const PROVIDER_TYPES = Object.freeze([
66
+ "llama-cpp",
67
+ "bedrock",
68
+ "anthropic",
69
+ "openai",
70
+ "xai",
71
+ "deepinfra",
72
+ "openclaw",
73
+ "azure-openai",
74
+ "openai-compatible",
75
+ ]);
76
+
77
+ /** Provider types with a native OpenClaw onboarding flag -- no base_url. */
78
+ export const NATIVE_PROVIDER_TYPES = Object.freeze(["anthropic", "openai", "xai", "deepinfra"]);
79
+
80
+ /** OpenClaw provider ids: lowercase letters, digits, dot, underscore, hyphen. */
81
+ export const OPENCLAW_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
82
+
83
+ /**
84
+ * The provider id OpenClaw itself knows this entry by -- what goes in front
85
+ * of the model (`<id>/<model>`) and what its credential is stored under.
86
+ * Every type is its own id except the generic `openclaw` one.
87
+ */
88
+ export function openclawProviderId(entry) {
89
+ return entry.provider === "openclaw" ? entry.openclaw_provider : entry.provider;
90
+ }
91
+
92
+ /** Types that register straight onto an existing OpenClaw provider id, with no custom endpoint to define first. */
93
+ export function isNativeProviderType(type) {
94
+ return type === "openclaw" || NATIVE_PROVIDER_TYPES.includes(type);
95
+ }
96
+
97
+ /** Provider types that register as a custom endpoint and therefore require
98
+ * `base_url` (bedrock's default is derived from its region at setup time
99
+ * the same way it already is today; azure-openai and openai-compatible have
100
+ * no sensible default at all -- every Azure deployment has its own URL). */
101
+ export const CUSTOM_ENDPOINT_PROVIDER_TYPES = Object.freeze(["bedrock", "azure-openai", "openai-compatible"]);
102
+
103
+ const idSchema = z
104
+ .string({ required_error: "is required", invalid_type_error: "must be a string" })
105
+ .regex(ID_RE, "must be 1-64 characters of letters, numbers, dot, underscore or hyphen");
106
+
107
+ const weightSchema = z
108
+ .number({ required_error: "is required", invalid_type_error: "must be a number" })
109
+ .positive("must be a positive number")
110
+ .finite("must be a finite number");
111
+
112
+ // Static, operator-declared ceiling on how many jobs may run against this
113
+ // one entry at once -- a stand-in for real rate-limit-aware admission (see
114
+ // README's dispatch-pool section for why that's out of scope for now).
115
+ const maxConcurrentSchema = z
116
+ .number({ invalid_type_error: "must be a number" })
117
+ .int("must be a whole number")
118
+ .positive("must be a positive number")
119
+ .default(2);
120
+
121
+ const authEnvSchema = z
122
+ .string({ required_error: "is required", invalid_type_error: "must be a string" })
123
+ .regex(
124
+ AUTH_ENV_NAME_RE,
125
+ "must be an environment variable NAME (uppercase letters, digits, underscores), never the credential itself",
126
+ );
127
+
128
+ const urlSchema = () =>
129
+ z
130
+ .string({ required_error: "is required", invalid_type_error: "must be a string" })
131
+ .url("must be a valid URL");
132
+
133
+ // Whether/how the entry's model uses a thinking/reasoning mode. `true`
134
+ // (default) mirrors config/common.env's NOMARMY_MODEL_THINKING -- pass
135
+ // through whatever `reasoning` level the job requested. `false` means this
136
+ // model has no reasoning mode at all, same as before this existed. A
137
+ // specific level ("low"/"medium"/"high") means this ENTRY always requests
138
+ // that level regardless of what the job asked for -- for a pool tier that
139
+ // exists specifically for harder work (e.g. "capable") and should never
140
+ // settle for less reasoning than its own declared floor. If the model
141
+ // doesn't actually support the requested level, OpenClaw says so in its own
142
+ // error and mcp/server.mjs's runOpenClaw retries once with whatever level
143
+ // OpenClaw names as supported (see parseUnsupportedThinkingError) -- so a
144
+ // level this schema accepts is never a guarantee the provider/model
145
+ // combination honors it today, only what to keep asking for.
146
+ const thinkingSchema = z.union([z.boolean(), z.enum(["low", "medium", "high"])]).default(true);
147
+
148
+ // Optional override for the model's real context window, in tokens. Absent
149
+ // (the common case), lib/dispatch-config.mjs looks this up from OpenClaw's
150
+ // own model catalog at dispatch time instead -- this field exists for the
151
+ // gap that lookup can't cover: a model newer than OpenClaw's cached catalog
152
+ // knows about yet (see lib/model-catalog.mjs), or an operator who wants to
153
+ // be more conservative than the model's rated maximum.
154
+ const contextWindowSchema = z
155
+ .number({ invalid_type_error: "must be a number" })
156
+ .int("must be a whole number")
157
+ .positive("must be a positive number")
158
+ .optional();
159
+
160
+ function hostedProviderSchema(providerType, { requireBaseUrl }) {
161
+ const shape = {
162
+ id: idSchema,
163
+ provider: z.literal(providerType),
164
+ model: requiredString(),
165
+ weight: weightSchema,
166
+ max_concurrent: maxConcurrentSchema,
167
+ auth_env: authEnvSchema,
168
+ thinking: thinkingSchema,
169
+ context_window: contextWindowSchema,
170
+ };
171
+ if (requireBaseUrl) shape.base_url = urlSchema();
172
+ return z.object(shape).strict();
173
+ }
174
+
175
+ const llamaCppEntrySchema = z
176
+ .object({
177
+ id: idSchema,
178
+ provider: z.literal("llama-cpp"),
179
+ // Omitted -> falls back to NOMARMY_WORKER_MODEL/NOMARMY_MODEL_THINKING,
180
+ // the exact same defaults a job gets today with no `pool` at all.
181
+ model: requiredString().optional(),
182
+ weight: weightSchema,
183
+ max_concurrent: maxConcurrentSchema,
184
+ })
185
+ .strict();
186
+
187
+ // Any other OpenClaw provider. `plugin` is the install spec when the
188
+ // provider isn't stock (e.g. "clawhub:@openclaw/deepseek-provider");
189
+ // `agents add` installs it before registering the key.
190
+ const genericOpenclawEntrySchema = hostedProviderSchema("openclaw", { requireBaseUrl: false }).extend({
191
+ openclaw_provider: z
192
+ .string({ required_error: "is required", invalid_type_error: "must be a string" })
193
+ .regex(OPENCLAW_PROVIDER_ID_RE, "must be an OpenClaw provider id (lowercase letters, digits, dot, underscore, hyphen)")
194
+ .refine((id) => !PROVIDER_TYPES.includes(id) || NATIVE_PROVIDER_TYPES.includes(id), "names a nomArmy provider type with its own setup -- use that type instead"),
195
+ plugin: z.string().regex(/^\S+$/, "must be one plugin install spec, e.g. clawhub:@openclaw/deepseek-provider").optional(),
196
+ }).strict();
197
+
198
+ export const providerEntrySchema = z.discriminatedUnion("provider", [
199
+ llamaCppEntrySchema,
200
+ genericOpenclawEntrySchema,
201
+ ...NATIVE_PROVIDER_TYPES.map((type) => hostedProviderSchema(type, { requireBaseUrl: false })),
202
+ ...CUSTOM_ENDPOINT_PROVIDER_TYPES.map((type) => hostedProviderSchema(type, { requireBaseUrl: true })),
203
+ ]);
204
+
205
+ export const poolsSchema = z.record(
206
+ z.string().min(1, "pool name must not be empty"),
207
+ z
208
+ .array(providerEntrySchema, { invalid_type_error: "must be an array of provider entries" })
209
+ .min(1, "must list at least one provider entry"),
210
+ );
211
+
212
+ export const dispatchConfigSchema = z
213
+ .object({
214
+ pools: poolsSchema,
215
+ })
216
+ .strict()
217
+ .superRefine((data, ctx) => {
218
+ // Ids double as the OpenClaw provider id and the per-entry admission/
219
+ // metrics key -- a collision across pools would be genuinely ambiguous,
220
+ // not just untidy, so this is a hard error rather than a lint.
221
+ const seenAt = new Map();
222
+ for (const [poolName, entries] of Object.entries(data.pools || {})) {
223
+ entries.forEach((entry, index) => {
224
+ if (seenAt.has(entry.id)) {
225
+ ctx.addIssue({
226
+ code: z.ZodIssueCode.custom,
227
+ path: ["pools", poolName, index, "id"],
228
+ message: `id "${entry.id}" is already used by pool "${seenAt.get(entry.id)}" -- provider ids must be unique across every pool`,
229
+ });
230
+ } else {
231
+ seenAt.set(entry.id, poolName);
232
+ }
233
+ });
234
+ }
235
+ });
236
+
237
+ /**
238
+ * Turn a ZodError from `dispatchConfigSchema` into readable `path: message`
239
+ * lines. A sibling of lib/schema.mjs's own formatIssues, kept separate
240
+ * because the discriminator values differ (PROVIDER_TYPES, not
241
+ * SERVICE_SOURCES) -- duplicating this one small function is cheaper and
242
+ * safer than parameterizing the shared one for a single call site.
243
+ */
244
+ export function formatDispatchIssues(error) {
245
+ const lines = [];
246
+ for (const issue of error.issues) {
247
+ const where = issue.path.length > 0 ? issue.path.join(".") : "config";
248
+ if (issue.code === "unrecognized_keys") {
249
+ const keys = (issue.keys || []).map((key) => `"${key}"`).join(", ");
250
+ lines.push(`${where}: unexpected field(s) ${keys}`);
251
+ continue;
252
+ }
253
+ if (issue.code === "invalid_union_discriminator") {
254
+ lines.push(`${where}: must be one of ${PROVIDER_TYPES.join(", ")}`);
255
+ continue;
256
+ }
257
+ if (issue.code === "invalid_type" && issue.received === "undefined") {
258
+ lines.push(`${where}: is required`);
259
+ continue;
260
+ }
261
+ lines.push(`${where}: ${issue.message}`);
262
+ }
263
+ return [...new Set(lines)];
264
+ }
265
+
266
+ /**
267
+ * Providers whose worker runs its own tools on this machine, outside
268
+ * nomArmy's sandbox. OpenClaw runs the Claude CLI (claude-cli) as the real
269
+ * `claude` binary on the host and approves its built-in Bash, Edit and
270
+ * Write itself: probed live, its shell reported Darwin, the real worktree
271
+ * path and network access (HTTP 200), where every other route (local, xai,
272
+ * openai via Codex, meta) reported Linux, /workspace and no network. A real
273
+ * Senti job on it ran `npm install` on the host and replaced the worktree's
274
+ * dependency link with macOS binaries. An api `anthropic` key runs through
275
+ * OpenClaw's own loop and is sandboxed like the rest.
276
+ */
277
+ export const HOST_TOOL_PROVIDERS = Object.freeze(["claude-cli"]);
278
+ export function agentRunsToolsOnHost(agent) {
279
+ return Boolean(agent && agent.kind === "subscription" && HOST_TOOL_PROVIDERS.includes(agent.provider));
280
+ }