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