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/lib/schema.mjs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// nomArmy `.nomarmy.yml` schema (v1.3).
|
|
2
|
+
//
|
|
3
|
+
// This module owns the *shape* of a repository's environment contract and
|
|
4
|
+
// nothing else: no filesystem access, no YAML. `lib/config.mjs` is the only
|
|
5
|
+
// place that touches either, so the parser stays swappable.
|
|
6
|
+
//
|
|
7
|
+
// Design rules the rest of the system depends on:
|
|
8
|
+
// * Every object is strict. A field belonging to another service source is a
|
|
9
|
+
// configuration error, never a silently ignored key.
|
|
10
|
+
// * `shared` and `remote` services are elevated: validation accepts them, but
|
|
11
|
+
// the caller is handed a structured list so it can require explicit policy
|
|
12
|
+
// approval before the job runs. See `collectElevated`.
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { armySchema } from "./army.mjs";
|
|
16
|
+
|
|
17
|
+
export const SERVICE_SOURCES = Object.freeze([
|
|
18
|
+
"compose",
|
|
19
|
+
"image",
|
|
20
|
+
"process",
|
|
21
|
+
"shared",
|
|
22
|
+
"remote",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** Sources that reach outside the job's own sandbox and need policy approval. */
|
|
26
|
+
export const ELEVATED_SERVICE_SOURCES = Object.freeze(["shared", "remote"]);
|
|
27
|
+
|
|
28
|
+
export const ENVIRONMENT_LEVELS = Object.freeze([
|
|
29
|
+
"none",
|
|
30
|
+
"basic",
|
|
31
|
+
"integration",
|
|
32
|
+
"e2e",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const RETENTION_ACTIONS = Object.freeze(["destroy", "logs", "retain"]);
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_RETENTION = Object.freeze({
|
|
38
|
+
success: "destroy",
|
|
39
|
+
failure: "logs",
|
|
40
|
+
debug: "retain",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** The field each service source owns, keyed by source. */
|
|
44
|
+
export const SERVICE_FIELDS = Object.freeze({
|
|
45
|
+
compose: "service",
|
|
46
|
+
image: "image",
|
|
47
|
+
process: "command",
|
|
48
|
+
shared: "endpoint",
|
|
49
|
+
remote: "endpoint_env",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// primitives
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
// Error messages omit the field name on purpose: `formatIssues` always prefixes
|
|
57
|
+
// the dotted path, so repeating it reads as stutter.
|
|
58
|
+
const requiredString = () =>
|
|
59
|
+
z
|
|
60
|
+
.string({ required_error: "is required", invalid_type_error: "must be a string" })
|
|
61
|
+
.refine((value) => value.trim().length > 0, { message: "must not be empty" });
|
|
62
|
+
|
|
63
|
+
const enumOf = (values) =>
|
|
64
|
+
z.enum(values, {
|
|
65
|
+
errorMap: () => ({ message: `must be one of ${values.join(", ")}` }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// A plain hostname: no scheme, no path, no port, no whitespace.
|
|
69
|
+
const HOSTNAME_RE =
|
|
70
|
+
/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
|
|
71
|
+
|
|
72
|
+
const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Why a value is not a usable bare hostname, or null when it is fine.
|
|
76
|
+
* Ordered so the most specific explanation wins (a URL trips scheme, not port).
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @returns {string|null}
|
|
79
|
+
*/
|
|
80
|
+
export function hostnameProblem(value) {
|
|
81
|
+
if (typeof value !== "string") return "must be a string hostname";
|
|
82
|
+
if (value.length === 0) return "must not be empty";
|
|
83
|
+
if (/\s/.test(value)) return `must not contain whitespace (got "${value}")`;
|
|
84
|
+
if (SCHEME_RE.test(value)) return `must not include a URI scheme (got "${value}")`;
|
|
85
|
+
if (value.includes("/")) return `must not include a path or slash (got "${value}")`;
|
|
86
|
+
if (value.includes(":")) return `must not include a port (got "${value}")`;
|
|
87
|
+
if (value.length > 253) return "must be at most 253 characters";
|
|
88
|
+
if (!HOSTNAME_RE.test(value)) return `must be a plain hostname (got "${value}")`;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const hostnameSchema = z
|
|
93
|
+
.string({ invalid_type_error: "must be a string hostname" })
|
|
94
|
+
.superRefine((value, ctx) => {
|
|
95
|
+
const problem = hostnameProblem(value);
|
|
96
|
+
if (problem) ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Service names become DNS names on the job network, so keep them boring.
|
|
100
|
+
const serviceNameSchema = z
|
|
101
|
+
.string()
|
|
102
|
+
.regex(
|
|
103
|
+
/^[A-Za-z0-9][A-Za-z0-9_.-]*$/,
|
|
104
|
+
"service name must start alphanumeric and use only letters, digits, hyphen, underscore or dot",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// services (discriminated on `source`, every branch strict)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export const composeServiceSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
source: z.literal("compose"),
|
|
114
|
+
service: requiredString(),
|
|
115
|
+
})
|
|
116
|
+
.strict();
|
|
117
|
+
|
|
118
|
+
export const imageServiceSchema = z
|
|
119
|
+
.object({
|
|
120
|
+
source: z.literal("image"),
|
|
121
|
+
image: requiredString(),
|
|
122
|
+
})
|
|
123
|
+
.strict();
|
|
124
|
+
|
|
125
|
+
export const processServiceSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
source: z.literal("process"),
|
|
128
|
+
command: requiredString(),
|
|
129
|
+
})
|
|
130
|
+
.strict();
|
|
131
|
+
|
|
132
|
+
export const sharedServiceSchema = z
|
|
133
|
+
.object({
|
|
134
|
+
source: z.literal("shared"),
|
|
135
|
+
endpoint: requiredString(),
|
|
136
|
+
})
|
|
137
|
+
.strict();
|
|
138
|
+
|
|
139
|
+
export const remoteServiceSchema = z
|
|
140
|
+
.object({
|
|
141
|
+
source: z.literal("remote"),
|
|
142
|
+
endpoint_env: requiredString(),
|
|
143
|
+
})
|
|
144
|
+
.strict();
|
|
145
|
+
|
|
146
|
+
export const serviceSchema = z.discriminatedUnion("source", [
|
|
147
|
+
composeServiceSchema,
|
|
148
|
+
imageServiceSchema,
|
|
149
|
+
processServiceSchema,
|
|
150
|
+
sharedServiceSchema,
|
|
151
|
+
remoteServiceSchema,
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// environment
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
// Which requirements file(s) a Python repo's sandbox image should install --
|
|
159
|
+
// see lib/sandbox-images.mjs's pythonRequirementsFor(). Paths are relative to
|
|
160
|
+
// the repository root; this schema only validates shape, never touches the
|
|
161
|
+
// filesystem (no "does this file exist" check belongs here).
|
|
162
|
+
export const pythonEnvironmentSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
requirements: z
|
|
165
|
+
.array(requiredString(), { invalid_type_error: "must be an array of strings" })
|
|
166
|
+
.min(1, "must list at least one requirements file"),
|
|
167
|
+
})
|
|
168
|
+
.strict();
|
|
169
|
+
|
|
170
|
+
export const environmentSchema = z
|
|
171
|
+
.object({
|
|
172
|
+
compose: z.object({ file: requiredString() }).strict().optional(),
|
|
173
|
+
services: z.record(serviceNameSchema, serviceSchema).optional(),
|
|
174
|
+
application: z
|
|
175
|
+
.object({
|
|
176
|
+
command: requiredString(),
|
|
177
|
+
healthcheck: requiredString().optional(),
|
|
178
|
+
})
|
|
179
|
+
.strict()
|
|
180
|
+
.optional(),
|
|
181
|
+
browser: z.object({ base_url: requiredString() }).strict().optional(),
|
|
182
|
+
allowed_hosts: z.array(hostnameSchema).optional(),
|
|
183
|
+
python: pythonEnvironmentSchema.optional(),
|
|
184
|
+
})
|
|
185
|
+
.strict();
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// verification
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export const verificationProfileSchema = z
|
|
192
|
+
.object({
|
|
193
|
+
environment: enumOf(ENVIRONMENT_LEVELS).default("none"),
|
|
194
|
+
commands: z
|
|
195
|
+
.array(requiredString(), {
|
|
196
|
+
required_error: "is required",
|
|
197
|
+
invalid_type_error: "must be an array of strings",
|
|
198
|
+
})
|
|
199
|
+
.min(1, "must list at least one command"),
|
|
200
|
+
})
|
|
201
|
+
.strict();
|
|
202
|
+
|
|
203
|
+
export const verificationSchema = z.record(
|
|
204
|
+
z.string().min(1, "verification profile name must not be empty"),
|
|
205
|
+
verificationProfileSchema,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// retention
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
export const environmentRetentionSchema = z
|
|
213
|
+
.object({
|
|
214
|
+
success: enumOf(RETENTION_ACTIONS).default(DEFAULT_RETENTION.success),
|
|
215
|
+
failure: enumOf(RETENTION_ACTIONS).default(DEFAULT_RETENTION.failure),
|
|
216
|
+
debug: enumOf(RETENTION_ACTIONS).default(DEFAULT_RETENTION.debug),
|
|
217
|
+
})
|
|
218
|
+
.strict()
|
|
219
|
+
.default({});
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// root
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
export const configSchema = z
|
|
226
|
+
.object({
|
|
227
|
+
environment: environmentSchema.optional(),
|
|
228
|
+
verification: verificationSchema.optional(),
|
|
229
|
+
environment_retention: environmentRetentionSchema,
|
|
230
|
+
// Who the General calls for what in this repo. Owned by lib/army.mjs,
|
|
231
|
+
// which merges it with the global and .nomarmy.local.yml layers.
|
|
232
|
+
army: armySchema.optional(),
|
|
233
|
+
})
|
|
234
|
+
.strict();
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// helpers
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** A fresh, empty elevated-services record. */
|
|
241
|
+
export function emptyElevated() {
|
|
242
|
+
return { shared: [], remote: [] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Structured list of every service that reaches outside the job sandbox.
|
|
247
|
+
* Declaration order is preserved so the list reads like the file.
|
|
248
|
+
* @param {object|null|undefined} config validated config
|
|
249
|
+
* @returns {{ shared: string[], remote: string[] }}
|
|
250
|
+
*/
|
|
251
|
+
export function collectElevated(config) {
|
|
252
|
+
const elevated = emptyElevated();
|
|
253
|
+
const services = config && config.environment && config.environment.services;
|
|
254
|
+
if (!services) return elevated;
|
|
255
|
+
for (const [name, definition] of Object.entries(services)) {
|
|
256
|
+
if (!definition) continue;
|
|
257
|
+
if (definition.source === "shared") elevated.shared.push(name);
|
|
258
|
+
else if (definition.source === "remote") elevated.remote.push(name);
|
|
259
|
+
}
|
|
260
|
+
return elevated;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Turn a ZodError into readable `path: message` lines. Never leak a raw dump.
|
|
265
|
+
* @param {import("zod").ZodError} error
|
|
266
|
+
* @returns {string[]}
|
|
267
|
+
*/
|
|
268
|
+
export function formatIssues(error) {
|
|
269
|
+
const lines = [];
|
|
270
|
+
for (const issue of error.issues) {
|
|
271
|
+
const where = issue.path.length > 0 ? issue.path.join(".") : "config";
|
|
272
|
+
if (issue.code === "unrecognized_keys") {
|
|
273
|
+
const keys = (issue.keys || []).map((key) => `"${key}"`).join(", ");
|
|
274
|
+
lines.push(`${where}: unexpected field(s) ${keys}`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (issue.code === "invalid_union_discriminator") {
|
|
278
|
+
lines.push(`${where}: must be one of ${SERVICE_SOURCES.join(", ")}`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (issue.code === "invalid_type" && issue.received === "undefined") {
|
|
282
|
+
lines.push(`${where}: is required`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
lines.push(`${where}: ${issue.message}`);
|
|
286
|
+
}
|
|
287
|
+
return [...new Set(lines)];
|
|
288
|
+
}
|