nomarmy 0.1.0-alpha.5 → 0.1.0-alpha.6
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/README.md +7 -5
- package/bin/nomarmy.mjs +146 -3
- package/harnesses/_template/README.md +27 -0
- package/harnesses/_template/harness.yml +26 -0
- package/harnesses/browser-playwright/README.md +35 -0
- package/harnesses/browser-playwright/fixture/package.json +1 -0
- package/harnesses/browser-playwright/fixture/page.html +1 -0
- package/harnesses/browser-playwright/fixture/page.spec.js +5 -0
- package/harnesses/browser-playwright/fixture/playwright.config.js +8 -0
- package/harnesses/browser-playwright/harness.yml +18 -0
- package/harnesses/go/README.md +45 -0
- package/harnesses/go/harness.yml +14 -0
- package/harnesses/mock-oidc/README.md +31 -0
- package/harnesses/mock-oidc/fixture/.nomarmy.yml +4 -0
- package/harnesses/mock-oidc/fixture/discovery.test.mjs +16 -0
- package/harnesses/mock-oidc/harness.yml +19 -0
- package/harnesses/node/README.md +53 -0
- package/harnesses/node/harness.yml +18 -0
- package/harnesses/python/README.md +46 -0
- package/harnesses/python/harness.yml +16 -0
- package/harnesses/rust/README.md +45 -0
- package/harnesses/rust/harness.yml +13 -0
- package/lib/admission.mjs +3 -0
- package/lib/army.mjs +7 -5
- package/lib/config.mjs +15 -0
- package/lib/coordinator-instructions.mjs +1 -1
- package/lib/doctor.mjs +62 -1
- package/lib/egress-proxy.mjs +116 -0
- package/lib/execute.mjs +8 -3
- package/lib/harness-schema.mjs +61 -0
- package/lib/harnesses.mjs +99 -0
- package/lib/openclaw-run.mjs +5 -6
- package/lib/podman-health.mjs +41 -0
- package/lib/propose.mjs +10 -11
- package/lib/registry-python.mjs +96 -0
- package/lib/registry-secrets.mjs +128 -0
- package/lib/repo-query.mjs +1 -1
- package/lib/sandbox-images.mjs +484 -81
- package/lib/sandbox-vm.mjs +32 -0
- package/lib/schema.mjs +1 -0
- package/lib/verification-artifacts.mjs +46 -0
- package/lib/verification-flow.mjs +46 -9
- package/lib/verification-network.mjs +66 -0
- package/lib/verify.mjs +305 -85
- package/mcp/server.mjs +14 -1
- package/package.json +4 -2
- package/playbooks/feature.md +1 -1
- package/scripts/generate-harness-docs.mjs +42 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
## TL;DR
|
|
14
14
|
|
|
15
|
-
1. **Have** Git, Node 20+ and [Podman](https://podman.io) (on macOS: `brew install podman && podman machine init && podman machine start
|
|
15
|
+
1. **Have** Git, Node 20+ and [Podman](https://podman.io) (on macOS: `brew install podman && podman machine init --memory 8192 && podman machine start`; Podman's 2 GiB default is too small for nomArmy's sandboxes).
|
|
16
16
|
2. **Install and set up:**
|
|
17
17
|
```bash
|
|
18
18
|
npm install -g nomarmy@alpha
|
|
@@ -67,6 +67,7 @@ Around that core: **agents** say where a job can run, the **army** says which ro
|
|
|
67
67
|
| [Agents and the army](https://github.com/rayson-tech/nomarmy/blob/main/docs/agents-and-army.md) | Where a job can run, who does what, usage limits, picking an agent |
|
|
68
68
|
| [`/feature` runs](https://github.com/rayson-tech/nomarmy/blob/main/docs/feature-runs.md) | A feature end to end, and watching what nomArmy is doing |
|
|
69
69
|
| [Your repository](https://github.com/rayson-tech/nomarmy/blob/main/docs/your-repo.md) | `.nomarmy.yml`, verification, languages and dependencies, what nomArmy checks |
|
|
70
|
+
| [Harnesses](https://github.com/rayson-tech/nomarmy/blob/main/docs/harnesses.md) | Ecosystem registry, detection, network levels, and requirements |
|
|
70
71
|
| [Configuration](https://github.com/rayson-tech/nomarmy/blob/main/docs/configuration.md) | Settings, swapping the local model, sizing, admission |
|
|
71
72
|
| [Reference](https://github.com/rayson-tech/nomarmy/blob/main/docs/reference.md) | Every CLI command and MCP tool |
|
|
72
73
|
| [Security posture](https://github.com/rayson-tech/nomarmy/blob/main/docs/security.md) | What the sandbox holds back, and the one exception |
|
|
@@ -88,9 +89,10 @@ A worker gets a writable git worktree inside a Podman sandbox and nothing else:
|
|
|
88
89
|
| Agents: api keys | Live-verified with xAI; other providers built to OpenClaw's documented interface |
|
|
89
90
|
| Agents: subscriptions | ChatGPT (Codex) and Muse Code sandboxed and live-verified; Claude live-verified, but its tools run on the host (scout and review by default) |
|
|
90
91
|
| The army and `/feature` | Driven by a real Claude Code General across three runs, about 18 implement jobs |
|
|
91
|
-
| Go, Rust, Python and Node repos |
|
|
92
|
-
|
|
|
93
|
-
| Browser
|
|
92
|
+
| Go, Rust, Python and Node repos, and mixed ones | Harness images live-verified: Go modules and Rust crates prefetched; npm, pnpm, yarn, bun and workspaces; pip, pyproject, uv and poetry |
|
|
93
|
+
| Fake services beside the app (mock login server, mock APIs) | Live-verified on a private network with no route out (the `services` harness level) |
|
|
94
|
+
| Browser tests (Playwright + Chromium) | Live-verified offline, with screenshots and traces kept as job evidence |
|
|
95
|
+
| Private registries and a verification-only network allowlist | Live-verified; each passed an independent security review |
|
|
94
96
|
|
|
95
97
|
What we've learned from real runs, including where delegating pays and where it doesn't, is in [docs/findings.md](https://github.com/rayson-tech/nomarmy/blob/main/docs/findings.md).
|
|
96
98
|
|
|
@@ -101,7 +103,7 @@ What we've learned from real runs, including where delegating pays and where it
|
|
|
101
103
|
- **Claude subscription token counts** come from the Claude CLI's own session log, since OpenClaw sees only the final reply. Totals include cache reads and writes, which make up most of an agent's prompt; each part is also kept separately.
|
|
102
104
|
- **Test-workaround detection is a flag, not a verdict**: a legitimate new skip still gets flagged.
|
|
103
105
|
- **Deploy-time failures need your own check.** See [Add a check for what unit tests can't see](https://github.com/rayson-tech/nomarmy/blob/main/docs/your-repo.md#nomarmyyml).
|
|
104
|
-
- **Node
|
|
106
|
+
- **Node private registries are not supported yet**: npm, pnpm, yarn and bun lockfiles and workspaces are supported, but the image build has no credentials for private registries (dependency plan step 8).
|
|
105
107
|
- **Verification needing services** (a database, a mock server) reports `not_run` instead of running without them. The compose parser doesn't resolve YAML anchors.
|
|
106
108
|
- **Same-host sandboxes**: the MCP server, OpenClaw and every job's sandbox run on the machine with the coordinator. Only the model can be elsewhere (an agent, or [a shared model server](https://github.com/rayson-tech/nomarmy/blob/main/docs/install.md#a-shared-model-server)).
|
|
107
109
|
|
package/bin/nomarmy.mjs
CHANGED
|
@@ -25,6 +25,9 @@ import { loadArmy, mergeArmy, describeArmy, readArmyFile, updateArmyInFile, assi
|
|
|
25
25
|
import { parseLlamaUrl } from "../lib/execution.mjs";
|
|
26
26
|
import { setupSteps, formatSetupSteps, runSetupPlaybook } from "../lib/setup-steps.mjs";
|
|
27
27
|
import { readUsageSnapshots } from "../lib/usage-limits.mjs";
|
|
28
|
+
import { pickMachine, planResize } from "../lib/sandbox-vm.mjs";
|
|
29
|
+
import { MIN_PODMAN_VM_MB } from "../lib/doctor.mjs";
|
|
30
|
+
import { liveLeases } from "../lib/slots.mjs";
|
|
28
31
|
import { ensureProviderConfig } from "../lib/openclaw-config.mjs";
|
|
29
32
|
import { recordProbeSuccess } from "../lib/health.mjs";
|
|
30
33
|
import { pruneJobRuntime } from "../lib/prune.mjs";
|
|
@@ -190,7 +193,7 @@ Usage: nomarmy <command> [options]
|
|
|
190
193
|
which agent the General is, in --global
|
|
191
194
|
(default) or --local
|
|
192
195
|
config paths where agents.yml and the three army layers live
|
|
193
|
-
jobs [--watch|--events|--prune] [--interval N] [--older-than DAYS]
|
|
196
|
+
jobs [--watch|--events|--prune|--wait <jobId>] [--interval N] [--older-than DAYS]
|
|
194
197
|
what's running across every session (agent, model, phase,
|
|
195
198
|
last tool call, files changed, heartbeat) and what just
|
|
196
199
|
finished; --watch redraws every N seconds (default 3);
|
|
@@ -198,7 +201,9 @@ Usage: nomarmy <command> [options]
|
|
|
198
201
|
finish (for Claude Code's background monitor; --json for
|
|
199
202
|
JSON lines); --prune removes the bulky runtime data
|
|
200
203
|
from finished jobs older than DAYS (default 2), keeping
|
|
201
|
-
their records, reports and any retained worktree
|
|
204
|
+
their records, reports and any retained worktree;
|
|
205
|
+
--wait <jobId> [--timeout <seconds>] blocks for one job
|
|
206
|
+
to finish (default timeout 1800; --json is supported)
|
|
202
207
|
health check what's likely to break a run before it does:
|
|
203
208
|
expiring logins, an outdated OpenClaw or plugin, roles
|
|
204
209
|
that can't be dispatched, an unloadable agents.yml,
|
|
@@ -211,6 +216,12 @@ Usage: nomarmy <command> [options]
|
|
|
211
216
|
(Re-)register the MCP server with one or more coordinators.
|
|
212
217
|
With no target and not --json, prompts an interactive
|
|
213
218
|
multi-select instead.
|
|
219
|
+
sandbox The Podman VM every sandbox shares (macOS, Windows): its
|
|
220
|
+
memory, disk and images. --memory <GiB> resizes it (stops,
|
|
221
|
+
sets, restarts; refused while jobs run); --prune removes
|
|
222
|
+
images no container uses (nomArmy rebuilds its own on
|
|
223
|
+
demand); --repair restores missing subordinate ID
|
|
224
|
+
ranges (refused while jobs run); --yes skips confirming
|
|
214
225
|
start <profile> Start local inference (wraps scripts/start-inference.sh).
|
|
215
226
|
stop <profile> Stop local inference (wraps scripts/stop-inference.sh).
|
|
216
227
|
uninstall Remove the MCP registration and install directory.
|
|
@@ -1783,6 +1794,77 @@ async function cmdConnect() {
|
|
|
1783
1794
|
function runScript(name, args = []) {
|
|
1784
1795
|
execFileSync("bash", [path.join(nomarmyRoot, "scripts", name), ...args], { cwd: nomarmyRoot, stdio: "inherit" });
|
|
1785
1796
|
}
|
|
1797
|
+
// `nomarmy sandbox`: see lib/sandbox-vm.mjs.
|
|
1798
|
+
async function cmdSandbox() {
|
|
1799
|
+
const stateRoot = process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents");
|
|
1800
|
+
const podman = (args, opts = {}) => spawnSync("podman", args, { encoding: "utf8", ...opts });
|
|
1801
|
+
const machine = process.platform === "linux" ? null : pickMachine(podman(["machine", "inspect"]).stdout);
|
|
1802
|
+
let images = null;
|
|
1803
|
+
try {
|
|
1804
|
+
const rows = JSON.parse(podman(["system", "df", "--format", "json"]).stdout || "[]");
|
|
1805
|
+
const row = rows.find((r) => /image/i.test(r.Type ?? ""));
|
|
1806
|
+
if (row) images = { count: row.Total ?? null, size: row.Size ?? null, reclaimable: row.Reclaimable ?? null };
|
|
1807
|
+
} catch { /* podman missing or old */ }
|
|
1808
|
+
const runningJobs = liveLeases(path.join(stateRoot, "leases")).length;
|
|
1809
|
+
const memoryGib = value("memory");
|
|
1810
|
+
|
|
1811
|
+
if (!memoryGib && !flag("prune") && !flag("repair")) {
|
|
1812
|
+
if (json) return out({ platform: process.platform, machine, images, runningJobs, minimumMb: MIN_PODMAN_VM_MB });
|
|
1813
|
+
console.log(c.bold("🍪 nomArmy sandbox"));
|
|
1814
|
+
if (process.platform === "linux") console.log("\nPodman runs natively on Linux: there's no VM to size.");
|
|
1815
|
+
else if (!machine) console.log(c.yellow("\nNo Podman machine found. Run: podman machine init && podman machine start"));
|
|
1816
|
+
else {
|
|
1817
|
+
const low = machine.memoryMb && machine.memoryMb < MIN_PODMAN_VM_MB;
|
|
1818
|
+
console.log(`\nVM ${machine.name} (${machine.state}): ${machine.cpus} CPUs, ${low ? c.red(`${machine.memoryMb / 1024} GiB memory`) : `${machine.memoryMb / 1024} GiB memory`}, ${machine.diskGb} GB disk`);
|
|
1819
|
+
if (low) console.log(c.yellow(` Too small: worker commands get cut off below ${MIN_PODMAN_VM_MB / 1024} GiB. Fix: nomarmy sandbox --memory 8`));
|
|
1820
|
+
}
|
|
1821
|
+
if (images) console.log(`Images: ${images.count}, ${images.size}${images.reclaimable ? `, ${images.reclaimable} reclaimable (nomarmy sandbox --prune)` : ""}`);
|
|
1822
|
+
console.log(c.dim(runningJobs ? `${runningJobs} nomArmy job(s) running.` : "No nomArmy jobs running."));
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const ask = async (question) => {
|
|
1827
|
+
if (flag("yes")) return true;
|
|
1828
|
+
if (!process.stdin.isTTY) throw new Error(`${question} Re-run with --yes to confirm without a terminal.`);
|
|
1829
|
+
const rl = createInterface({ input, output });
|
|
1830
|
+
try { return await confirm(rl, question, { defaultYes: false }); } finally { rl.close(); }
|
|
1831
|
+
};
|
|
1832
|
+
const runPodman = (args) => {
|
|
1833
|
+
console.log(c.dim(`$ podman ${args.join(" ")}`));
|
|
1834
|
+
const r = podman(args, { stdio: "inherit" });
|
|
1835
|
+
if (r.status !== 0) throw new Error(`podman ${args.join(" ")} failed (exit ${r.status ?? "none"}).`);
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
if (flag("repair")) {
|
|
1839
|
+
// Restore the subordinate ID ranges a Podman machine ships with, only
|
|
1840
|
+
// where they're missing, then let Podman re-read them.
|
|
1841
|
+
if (process.platform === "linux") throw new Error("On Linux, add a range for your user to /etc/subuid and /etc/subgid (e.g. `sudo usermod --add-subuids 100000-1099999 --add-subgids 100000-1099999 $USER`), then run `podman system migrate`.");
|
|
1842
|
+
if (!machine) throw new Error("No Podman machine found. Run: podman machine init && podman machine start");
|
|
1843
|
+
// podman system migrate stops every container, a running job's sandbox included.
|
|
1844
|
+
if (runningJobs > 0) throw new Error(`${runningJobs} nomArmy job(s) are running; the repair restarts Podman's containers and would kill their sandboxes. Run it when they're done.`);
|
|
1845
|
+
const script = 'set -e; restored=0; for f in /etc/subuid /etc/subgid; do if ! grep -q "^$(id -un):" "$f" 2>/dev/null; then echo "$(id -un):100000:1000000" | sudo tee -a "$f" >/dev/null; echo "restored $f"; restored=1; else echo "$f already has a range"; fi; done; if [ "$restored" = 1 ]; then podman system migrate; else echo "nothing to repair"; fi';
|
|
1846
|
+
if (!(await ask(`Restore missing subordinate ID ranges in the ${machine.name} VM and run podman system migrate?`))) { console.log(c.dim("Nothing changed.")); return; }
|
|
1847
|
+
const r = spawnSync("podman", ["machine", "ssh", machine.name, script], { stdio: "inherit" });
|
|
1848
|
+
if (r.status !== 0) throw new Error(`the repair failed (exit ${r.status ?? "none"}).`);
|
|
1849
|
+
console.log(c.green("✓ Done. Check with: nomarmy doctor"));
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
if (memoryGib) {
|
|
1854
|
+
const plan = planResize({ gib: memoryGib, machine, hostMemoryMb: os.totalmem() / 1024 / 1024, runningJobs });
|
|
1855
|
+
if (!plan.ok) throw new Error(plan.problems.join("; "));
|
|
1856
|
+
for (const w of plan.warnings) console.log(c.yellow(`⚠ ${w}`));
|
|
1857
|
+
if (!(await ask(`Restart the Podman VM with ${memoryGib} GiB (from ${machine.memoryMb / 1024})?`))) { console.log(c.dim("Nothing changed.")); return; }
|
|
1858
|
+
for (const args of plan.commands) runPodman(args);
|
|
1859
|
+
console.log(c.green(`✓ The Podman VM now has ${memoryGib} GiB.`));
|
|
1860
|
+
}
|
|
1861
|
+
if (flag("prune")) {
|
|
1862
|
+
if (runningJobs > 0) throw new Error(`${runningJobs} nomArmy job(s) are running; prune when they're done.`);
|
|
1863
|
+
if (!(await ask(`Remove every image no container uses${images?.reclaimable ? ` (about ${images.reclaimable})` : ""}? nomArmy rebuilds its own when a job needs them.`))) { console.log(c.dim("Nothing removed.")); return; }
|
|
1864
|
+
runPodman(["image", "prune", "--all", "--force"]);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1786
1868
|
async function cmdStart() { console.log(c.bold("🍪 Starting inference...\n")); runScript("start-inference.sh", argv.slice(1)); }
|
|
1787
1869
|
async function cmdStop() { runScript("stop-inference.sh", argv.slice(1)); }
|
|
1788
1870
|
function safeDu(dir) {
|
|
@@ -2369,6 +2451,66 @@ async function streamJobEvents() {
|
|
|
2369
2451
|
}
|
|
2370
2452
|
}
|
|
2371
2453
|
|
|
2454
|
+
/** Wait for one job in the shared, cross-session state directory. */
|
|
2455
|
+
async function waitForJobCli() {
|
|
2456
|
+
const requested = value("wait");
|
|
2457
|
+
const jobId = requested ? path.basename(requested) : null;
|
|
2458
|
+
const timeoutSeconds = Number(value("timeout", "1800"));
|
|
2459
|
+
if (!jobId || jobId !== requested) {
|
|
2460
|
+
if (json) out({ error: "--wait needs a job id" });
|
|
2461
|
+
else console.error("nomarmy jobs: --wait needs a job id");
|
|
2462
|
+
process.exitCode = 2;
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
|
2466
|
+
if (json) out({ error: "--timeout must be a non-negative number of seconds" });
|
|
2467
|
+
else console.error("nomarmy jobs: --timeout must be a non-negative number of seconds");
|
|
2468
|
+
process.exitCode = 2;
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const jobDir = path.join(jobsRootDir(), jobId);
|
|
2472
|
+
const lease = path.join(agentStateRoot(), "leases", `${jobId}.json`);
|
|
2473
|
+
if (!fs.existsSync(jobDir) && !fs.existsSync(lease)) {
|
|
2474
|
+
if (json) out({ error: `unknown job id: ${jobId}` });
|
|
2475
|
+
else console.error(`nomarmy jobs: unknown job id: ${jobId}`);
|
|
2476
|
+
process.exitCode = 2;
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
2480
|
+
for (;;) {
|
|
2481
|
+
const status = readJsonSafe(path.join(jobDir, "status.json")) ?? {};
|
|
2482
|
+
const meta = readJsonSafe(path.join(jobDir, "metadata.json")) ?? {};
|
|
2483
|
+
if (status.state === "finished" || meta.outcome) {
|
|
2484
|
+
const issues = Array.isArray(meta.issues) ? meta.issues : Array.isArray(status.issues) ? status.issues : [];
|
|
2485
|
+
const result = {
|
|
2486
|
+
jobId,
|
|
2487
|
+
outcome: meta.outcome ?? status.outcome ?? null,
|
|
2488
|
+
coordinatorStatus: meta.coordinatorStatus ?? status.coordinatorStatus ?? null,
|
|
2489
|
+
branch: meta.branch ?? status.branch ?? null,
|
|
2490
|
+
commit: meta.commit?.sha ?? meta.commit ?? status.commit?.sha ?? status.commit ?? null,
|
|
2491
|
+
issues,
|
|
2492
|
+
};
|
|
2493
|
+
if (json) out(result);
|
|
2494
|
+
else {
|
|
2495
|
+
const firstIssue = issues[0];
|
|
2496
|
+
const issueText = firstIssue == null ? null : typeof firstIssue === "string" ? firstIssue : firstIssue.message ?? JSON.stringify(firstIssue);
|
|
2497
|
+
console.log([result.jobId, result.outcome ?? "unknown", result.coordinatorStatus ?? "unknown",
|
|
2498
|
+
result.branch ? `branch=${result.branch}` : null, result.commit ? `commit=${result.commit}` : null,
|
|
2499
|
+
issueText ? `issue=${issueText}` : null].filter(Boolean).join(" "));
|
|
2500
|
+
}
|
|
2501
|
+
process.exitCode = result.coordinatorStatus === "complete" ? 0 : 1;
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
if (Date.now() >= deadline) {
|
|
2505
|
+
if (json) out({ error: `timed out waiting for job ${jobId}` });
|
|
2506
|
+
else console.error(`nomarmy jobs: timed out waiting for job ${jobId}`);
|
|
2507
|
+
process.exitCode = 2;
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(2000, Math.max(1, deadline - Date.now()))));
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2372
2514
|
/**
|
|
2373
2515
|
* `nomarmy jobs --prune [--older-than DAYS]`: remove runtime/ (per-job npm
|
|
2374
2516
|
* cache, harness state such as Codex's, OpenClaw's transcript) from
|
|
@@ -2385,6 +2527,7 @@ function pruneJobRuntimeCli() {
|
|
|
2385
2527
|
}
|
|
2386
2528
|
|
|
2387
2529
|
async function cmdJobs() {
|
|
2530
|
+
if (flag("wait")) return waitForJobCli();
|
|
2388
2531
|
if (flag("events")) return streamJobEvents();
|
|
2389
2532
|
if (flag("prune")) return pruneJobRuntimeCli();
|
|
2390
2533
|
if (json) return out(collectJobs());
|
|
@@ -2431,7 +2574,7 @@ async function cmdStatusline() {
|
|
|
2431
2574
|
process.stdout.write(`${statusLineText({ session })}\n`);
|
|
2432
2575
|
}
|
|
2433
2576
|
|
|
2434
|
-
const commands = { scan: cmdScan, validate: cmdValidate, sizing: cmdSizing, init: cmdInit, setup: cmdSetup, install: cmdInstall, model: cmdModel, agents: cmdAgents, army: cmdArmy, jobs: cmdJobs, statusline: cmdStatusline, health: cmdHealth, config: cmdConfig, update: cmdUpdate, connect: cmdConnect, start: cmdStart, stop: cmdStop, uninstall: cmdUninstall, help: () => usage(0) };
|
|
2577
|
+
const commands = { scan: cmdScan, validate: cmdValidate, sizing: cmdSizing, init: cmdInit, setup: cmdSetup, install: cmdInstall, model: cmdModel, agents: cmdAgents, army: cmdArmy, jobs: cmdJobs, statusline: cmdStatusline, health: cmdHealth, config: cmdConfig, update: cmdUpdate, connect: cmdConnect, sandbox: cmdSandbox, start: cmdStart, stop: cmdStop, uninstall: cmdUninstall, help: () => usage(0) };
|
|
2435
2578
|
// doctor command
|
|
2436
2579
|
async function cmdDoctor() {
|
|
2437
2580
|
// Import lazily to avoid circular dependencies
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Harness name
|
|
2
|
+
|
|
3
|
+
Write a short summary of what this harness provides.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
List the markers and packages that match. Explain any `after` layers.
|
|
8
|
+
|
|
9
|
+
## Installed today
|
|
10
|
+
|
|
11
|
+
List tools and dependencies, versions, build commands, and known gaps.
|
|
12
|
+
|
|
13
|
+
## Verification and network
|
|
14
|
+
|
|
15
|
+
Document each proposed profile. Declare the lowest network level needed.
|
|
16
|
+
For allowlist verification, document operator opt-in and dedicated test tenants
|
|
17
|
+
with throwaway credentials, never production. Workers always remain offline.
|
|
18
|
+
This registry does not enable services or allowlist networking yet.
|
|
19
|
+
|
|
20
|
+
## Requirements and artifacts
|
|
21
|
+
|
|
22
|
+
Explain memory, shared memory, KVM, and artifact globs, if any.
|
|
23
|
+
|
|
24
|
+
## Fixture
|
|
25
|
+
|
|
26
|
+
Add a tiny repository under `fixture/`. Describe how to verify it and its
|
|
27
|
+
expected results. Keep credentials and host-specific paths out of fixtures.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copy this folder to harnesses/<name>. Underscore folders are ignored.
|
|
2
|
+
# name: example-harness # Required kebab-case name, equal to the folder name.
|
|
3
|
+
# summary: Short description of what this installs.
|
|
4
|
+
# detect: # Any matching rule applies. Paths are relative to the repo.
|
|
5
|
+
# - file: example.config.json
|
|
6
|
+
# - package: example-package # A Node dependency in a discovered package.json.
|
|
7
|
+
# - lockfile: example.lock
|
|
8
|
+
# after: [node] # Existing harnesses that must precede this one.
|
|
9
|
+
# image: # Choose declarative apt/run OR builtin, never both.
|
|
10
|
+
# apt: [example-package] # OS packages installed at image build time.
|
|
11
|
+
# run: ["example-install --frozen"] # Build commands, not worker commands.
|
|
12
|
+
# # builtin: node # Install logic implemented in lib/.
|
|
13
|
+
# verification: # Proposed profiles mapped to commands or command lists.
|
|
14
|
+
# quick: example-test
|
|
15
|
+
# full: [example-lint, example-test]
|
|
16
|
+
# artifacts: [test-results/**] # Repository-relative artifact globs.
|
|
17
|
+
# requires: # Optional resource requirements.
|
|
18
|
+
# memoryMb: 1024
|
|
19
|
+
# shmMb: 512
|
|
20
|
+
# kvm: false
|
|
21
|
+
# network: none # Default; services and allowlist are future verification levels.
|
|
22
|
+
# services: [] # Service names; omit entirely for network none, even if empty.
|
|
23
|
+
# suggestedRole: # Optional role suggestion.
|
|
24
|
+
# name: example-qa
|
|
25
|
+
# description: Runs the example suite.
|
|
26
|
+
# docs: README.md # Default; path relative to this harness folder.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Playwright browser harness
|
|
2
|
+
|
|
3
|
+
Installs Chromium and its OS dependencies using the repository's installed
|
|
4
|
+
@playwright/test version, after the Node dependency layer. Commit a supported
|
|
5
|
+
Node lockfile. Root and discovered nested packages are supported; browsers live
|
|
6
|
+
in /ms-playwright and are readable by the node user. Installation failure leaves
|
|
7
|
+
a .nomarmy-playwright-install-failed marker in /ms-playwright.
|
|
8
|
+
|
|
9
|
+
Select the browser verification profile (npx playwright test) in .nomarmy.yml:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
verification:
|
|
13
|
+
browser:
|
|
14
|
+
commands: [npx playwright test]
|
|
15
|
+
environment: none
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Allow at least 1024 MB memory. nomArmy's verification container allocates 512 MB
|
|
19
|
+
/dev/shm (or the largest shared-memory requirement of the matched harnesses).
|
|
20
|
+
For OpenClaw-managed worker sandboxes, unless OpenClaw exposes a shm setting,
|
|
21
|
+
launch Chromium with --disable-dev-shm-usage via Playwright launchOptions, as
|
|
22
|
+
the fixture does.
|
|
23
|
+
|
|
24
|
+
The fixture opens its own static HTML with file://: the test needs no network.
|
|
25
|
+
Install its npm dependencies and Chromium before running offline.
|
|
26
|
+
|
|
27
|
+
Screenshots, traces and reports under test-results/** and playwright-report/**
|
|
28
|
+
become job evidence under artifacts/, listed in verification.artifacts.
|
|
29
|
+
Collection is limited to 200 files and 50 MB; a cap is reported in verification.
|
|
30
|
+
Most of the time the General checks the UI itself after merging. This harness
|
|
31
|
+
is for repositories with a real end-to-end suite as their gate.
|
|
32
|
+
|
|
33
|
+
## Running the fixture
|
|
34
|
+
|
|
35
|
+
`fixture/` has no lockfile on purpose: with one, nomArmy's own repository would match this harness and every nomArmy job would build Chromium into its image. To try it, copy the folder somewhere, run `npm install --package-lock-only` there (with network), commit, and verify it with the `browser` profile. Checked this way: headless Chromium passes offline, and the screenshot and trace land in the job's `artifacts/`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"private":true,"type":"module","scripts":{"test":"playwright test"},"devDependencies":{"@playwright/test":"1.55.1"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><title>Offline fixture</title><h1>Browser ready</h1>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineConfig } from '@playwright/test';
|
|
2
|
+
export default defineConfig({
|
|
3
|
+
testDir: '.',
|
|
4
|
+
testMatch: 'page.spec.js',
|
|
5
|
+
reporter: [['html', { open: 'never' }]],
|
|
6
|
+
use: { browserName: 'chromium', screenshot: 'on', trace: 'on',
|
|
7
|
+
launchOptions: { args: ['--disable-dev-shm-usage'] } },
|
|
8
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: browser-playwright
|
|
2
|
+
summary: Offline Chromium end-to-end tests with the repository's Playwright version
|
|
3
|
+
detect:
|
|
4
|
+
- package: '@playwright/test'
|
|
5
|
+
- file: playwright.config.ts
|
|
6
|
+
- file: playwright.config.js
|
|
7
|
+
- file: playwright.config.mjs
|
|
8
|
+
after: [node]
|
|
9
|
+
image:
|
|
10
|
+
builtin: playwright
|
|
11
|
+
verification:
|
|
12
|
+
browser: npx playwright test
|
|
13
|
+
artifacts: [test-results/**, playwright-report/**]
|
|
14
|
+
requires:
|
|
15
|
+
memoryMb: 1024
|
|
16
|
+
shmMb: 512
|
|
17
|
+
network: none
|
|
18
|
+
docs: README.md
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Go harness
|
|
2
|
+
|
|
3
|
+
Go toolchain with prefetched modules for offline verification.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
Matches `go.mod` or `go.work` at the repository root.
|
|
8
|
+
|
|
9
|
+
## Installed today
|
|
10
|
+
|
|
11
|
+
Installs Go 1.27.1 from the upstream tarball for amd64 or arm64, on the Node 24 Debian bookworm base with Python 3 and pip. Sets GOPATH for the node user.
|
|
12
|
+
|
|
13
|
+
At image build, copies root `go.mod` and `go.sum` (when present), plus `go.work`, `go.work.sum` and each repository-contained workspace module’s manifests. Runs `go mod download` as node into the default module cache, `/home/node/go/pkg/mod`. Repositories with `vendor/` skip this download. A failed download leaves `/deps/go/.nomarmy-go-mod-download-failed` without failing the image build. Runtime sets `GOPROXY=off` and `GOSUMDB=off`; no `GOFLAGS` override is set, preserving Go’s default module behavior. Copied metadata is hashed into the image tag, so dependency changes rebuild the cache. The layer composes with Node dependencies in mixed repositories.
|
|
14
|
+
|
|
15
|
+
## Verification and network
|
|
16
|
+
|
|
17
|
+
Proposes `go test ./...` as the `quick` profile. Network level: `none`. Matched harnesses compose the sandbox image used by workers and verification. The verification profile is a proposal; the registry does not change job profiles or network access.
|
|
18
|
+
|
|
19
|
+
## Remaining limitations
|
|
20
|
+
|
|
21
|
+
Private modules and registries use the build-only credentials described below; no credentials are copied into the image. A failure marker means the cache may be incomplete and offline verification can still fail. Workspace modules and local replacements outside the repository are not copied.
|
|
22
|
+
|
|
23
|
+
## Requirements and artifacts
|
|
24
|
+
|
|
25
|
+
No additional resource requirements or artifact globs are declared.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Private registries
|
|
29
|
+
|
|
30
|
+
Use `registries:` in the gitignored `.nomarmy.local.yml` only; the committed
|
|
31
|
+
config rejects it. Values are host credential **file paths**, never tokens.
|
|
32
|
+
Use `go: { netrc: ~/.netrc, private: "github.com/acme/*" }`. Only `go mod download` receives the node-owned netrc plus RUN-local GOPRIVATE and GONOSUMDB; vendored builds need no download mount.
|
|
33
|
+
|
|
34
|
+
Credentials stay on the host and enter Podman only as build-secret mounts;
|
|
35
|
+
no credential files are copied into the context or passed to sandbox jobs.
|
|
36
|
+
Use a **read-only, least-privilege token** and reviewed dependencies. Secrets
|
|
37
|
+
are accessible to install code during that RUN, so malicious packages are not
|
|
38
|
+
made safe by a mount. Credential rotation invalidates both tag and install
|
|
39
|
+
cache. Credentialed install output/build-error details are withheld.
|
|
40
|
+
|
|
41
|
+
See [Private registries](../../docs/your-repo.md#private-registries) for the
|
|
42
|
+
configuration and required manual canary check: inspect `podman history
|
|
43
|
+
--no-trunc`, image configuration, the exported filesystem **and every saved
|
|
44
|
+
image layer**, plus failure logs/job records, for absent secret bytes. Unit
|
|
45
|
+
tests stub Podman; live isolation needs independent security review.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: go
|
|
2
|
+
summary: Go toolchain with prefetched modules for offline verification
|
|
3
|
+
detect:
|
|
4
|
+
- file: go.mod
|
|
5
|
+
- file: go.work
|
|
6
|
+
after: []
|
|
7
|
+
image:
|
|
8
|
+
builtin: go
|
|
9
|
+
verification:
|
|
10
|
+
quick: go test ./...
|
|
11
|
+
artifacts: []
|
|
12
|
+
requires: {}
|
|
13
|
+
network: none
|
|
14
|
+
docs: README.md
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Mock OIDC
|
|
2
|
+
|
|
3
|
+
Enable explicitly in your repository's `.nomarmy.yml` (there are no detection rules):
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
harnesses: [mock-oidc]
|
|
7
|
+
verification:
|
|
8
|
+
auth:
|
|
9
|
+
commands: [npm test]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Point your application's issuer/discovery configuration at `OIDC_ISSUER` or
|
|
13
|
+
`OAUTH2_ISSUER`, both exported as `http://mock-oidc:8080/default` during nomArmy
|
|
14
|
+
verification. Configure your test client ID, redirect URI and audience to match
|
|
15
|
+
what your application expects; allow HTTP only in this test configuration.
|
|
16
|
+
|
|
17
|
+
The pinned NAV mock OAuth2 server supplies discovery, signing keys, token and
|
|
18
|
+
mock authorization flows. It is not an Okta/Auth0 tenant: it does not prove real
|
|
19
|
+
identity, MFA, tenant policies, upstream federation or production compatibility.
|
|
20
|
+
Never use real credentials with this fake.
|
|
21
|
+
|
|
22
|
+
nomArmy pulls the pinned image on the host if missing, then starts it without
|
|
23
|
+
published ports on a per-run `--internal` Podman network. Discovery must return
|
|
24
|
+
2xx within 30 seconds before commands run. Containers and the network are
|
|
25
|
+
removed even if verification fails. Workers remain `--network none`; only
|
|
26
|
+
nomArmy's own verification can reach the fake. Env maps in harness manifests
|
|
27
|
+
are non-secret literals, not secret references or host-environment imports.
|
|
28
|
+
|
|
29
|
+
`fixture/` is a tiny Node-built-ins-only repository. Run its `quick` profile
|
|
30
|
+
through nomArmy verification. It fetches discovery over the private network;
|
|
31
|
+
it requires no internet or npm packages. It is not a host-side unit test.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
test('private mock issuer discovery', async () => {
|
|
5
|
+
const issuer = process.env.OIDC_ISSUER;
|
|
6
|
+
assert.equal(issuer, 'http://mock-oidc:8080/default');
|
|
7
|
+
const response = await fetch(`${issuer}/.well-known/openid-configuration`, {
|
|
8
|
+
redirect: 'error', signal: AbortSignal.timeout(5000),
|
|
9
|
+
});
|
|
10
|
+
assert.equal(response.status, 200);
|
|
11
|
+
const document = await response.json();
|
|
12
|
+
assert.equal(document.issuer, issuer);
|
|
13
|
+
assert.equal(new URL(document.jwks_uri).origin, new URL(issuer).origin);
|
|
14
|
+
assert.equal(new URL(document.authorization_endpoint).origin, new URL(issuer).origin);
|
|
15
|
+
assert.equal(new URL(document.token_endpoint).origin, new URL(issuer).origin);
|
|
16
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: mock-oidc
|
|
2
|
+
summary: Mock OAuth2 and OIDC discovery and login on an isolated verification network
|
|
3
|
+
detect: []
|
|
4
|
+
image:
|
|
5
|
+
apt: []
|
|
6
|
+
run: []
|
|
7
|
+
network: services
|
|
8
|
+
services:
|
|
9
|
+
- name: mock-oidc
|
|
10
|
+
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
|
|
11
|
+
port: 8080
|
|
12
|
+
env:
|
|
13
|
+
SERVER_PORT: '8080'
|
|
14
|
+
health: /default/.well-known/openid-configuration
|
|
15
|
+
env:
|
|
16
|
+
OIDC_ISSUER: http://mock-oidc:8080/default
|
|
17
|
+
OAUTH2_ISSUER: http://mock-oidc:8080/default
|
|
18
|
+
verification:
|
|
19
|
+
oidc: node discovery.test.mjs
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Node harness
|
|
2
|
+
|
|
3
|
+
Node dependencies and workspaces installed with npm, pnpm, yarn or bun.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
Matches `package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock` or `bun.lockb` at the root or in discovered package directories.
|
|
8
|
+
|
|
9
|
+
## Installed today
|
|
10
|
+
|
|
11
|
+
Installs dependencies as the `node` user under `/deps` at image build time, on Node 24 / Debian bookworm:
|
|
12
|
+
|
|
13
|
+
- npm: `npm ci --no-audit --no-fund` (unchanged).
|
|
14
|
+
- pnpm: Corepack and `pnpm install --frozen-lockfile`.
|
|
15
|
+
- Yarn classic: Corepack and `yarn install --frozen-lockfile`.
|
|
16
|
+
- Yarn Berry: detected by `.yarnrc.yml` or lockfile `__metadata`; `YARN_NODE_LINKER=node-modules yarn install --immutable`.
|
|
17
|
+
- Bun: installed globally at build time, then `bun install --frozen-lockfile`.
|
|
18
|
+
|
|
19
|
+
Corepack respects `packageManager` in the manifest. Without a pin, Yarn defaults to classic or current stable Berry according to detection. Failed installs leave `.nomarmy-npm-ci-failed` for npm or `.nomarmy-<manager>-install-failed` for other managers (`yarn-berry` for Berry).
|
|
20
|
+
|
|
21
|
+
A manifest's `workspaces` array (or `workspaces.packages`) or `pnpm-workspace.yaml` causes one install at that workspace root. Member manifests are copied at their relative paths, including members without their own lockfile; workspace members are not installed twice. Workspace globs support `*`, `**`, `?`, brace alternatives and exclusions. Non-workspace packages retain separate installs.
|
|
22
|
+
|
|
23
|
+
Root dependencies resolve through `/node_modules`; workspace roots and members also receive worktree links into `/deps`, preserving pnpm's relative store links. The root binary PATH remains `/deps/node_modules/.bin`. Copied manifests, lockfiles and workspace/config files participate in the image tag.
|
|
24
|
+
|
|
25
|
+
Private registries use operator-local build secrets; see the configuration and Yarn Berry limitation below.
|
|
26
|
+
|
|
27
|
+
## Verification and network
|
|
28
|
+
|
|
29
|
+
Proposes `npm test` as the `quick` profile. Network level: `none`. Matched harnesses compose the sandbox image used by workers and verification. The verification profile is a proposal; the registry does not change job profiles or network access.
|
|
30
|
+
|
|
31
|
+
## Requirements and artifacts
|
|
32
|
+
|
|
33
|
+
No additional resource requirements or artifact globs are declared.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Private registries
|
|
37
|
+
|
|
38
|
+
Use `registries:` in the gitignored `.nomarmy.local.yml` only; the committed
|
|
39
|
+
config rejects it. Values are host credential **file paths**, never tokens.
|
|
40
|
+
npm, pnpm, Yarn Classic and bun read the mounted `.npmrc` as node (uid 1000). Credentialed Yarn Berry builds are currently rejected because Berry does not read `.npmrc`.
|
|
41
|
+
|
|
42
|
+
Credentials stay on the host and enter Podman only as build-secret mounts;
|
|
43
|
+
no credential files are copied into the context or passed to sandbox jobs.
|
|
44
|
+
Use a **read-only, least-privilege token** and reviewed dependencies. Secrets
|
|
45
|
+
are accessible to install code during that RUN, so malicious packages are not
|
|
46
|
+
made safe by a mount. Credential rotation invalidates both tag and install
|
|
47
|
+
cache. Credentialed install output/build-error details are withheld.
|
|
48
|
+
|
|
49
|
+
See [Private registries](../../docs/your-repo.md#private-registries) for the
|
|
50
|
+
configuration and required manual canary check: inspect `podman history
|
|
51
|
+
--no-trunc`, image configuration, the exported filesystem **and every saved
|
|
52
|
+
image layer**, plus failure logs/job records, for absent secret bytes. Unit
|
|
53
|
+
tests stub Podman; live isolation needs independent security review.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: node
|
|
2
|
+
summary: Node dependencies and workspaces installed with npm, pnpm, yarn or bun
|
|
3
|
+
detect:
|
|
4
|
+
- lockfile: package-lock.json
|
|
5
|
+
- lockfile: npm-shrinkwrap.json
|
|
6
|
+
- lockfile: pnpm-lock.yaml
|
|
7
|
+
- lockfile: yarn.lock
|
|
8
|
+
- lockfile: bun.lock
|
|
9
|
+
- lockfile: bun.lockb
|
|
10
|
+
after: []
|
|
11
|
+
image:
|
|
12
|
+
builtin: node
|
|
13
|
+
verification:
|
|
14
|
+
quick: npm test
|
|
15
|
+
artifacts: []
|
|
16
|
+
requires: {}
|
|
17
|
+
network: none
|
|
18
|
+
docs: README.md
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python harness
|
|
2
|
+
|
|
3
|
+
Python dependencies from uv, Poetry, pyproject, or requirements files.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
Matches `uv.lock`, `poetry.lock`, `pyproject.toml`, or `requirements.txt` at the repository root. Explicit `environment.python.requirements` in `.nomarmy.yml` wins; otherwise the install precedence is uv.lock, poetry.lock (or `[tool.poetry]`), `[project]` in pyproject.toml, then requirements.txt.
|
|
8
|
+
|
|
9
|
+
## Installed dependencies
|
|
10
|
+
|
|
11
|
+
Installs Python 3, pip, venv, and build-essential alongside the Node 24 base. Dependency downloads happen at image build time, not in the offline sandbox.
|
|
12
|
+
|
|
13
|
+
- **uv:** installs uv and runs `uv sync --frozen --no-install-project --all-groups`, including dev groups, from the copied pyproject.toml and uv.lock.
|
|
14
|
+
- **Poetry:** installs Poetry and runs `poetry install --no-root --no-interaction` from pyproject.toml and poetry.lock when present. Adds `--with dev` only when a Poetry dev group exists. Legacy dev dependencies follow Poetry's default behavior.
|
|
15
|
+
- **Plain pyproject:** uses Python's TOML parser to install `[project].dependencies` and optional `test`, `tests`, and `dev` dependencies with pip, without installing the project itself. A lockfile makes dependency resolution reproducible; unlocked pyproject dependencies may resolve differently on rebuild.
|
|
16
|
+
- **Requirements:** retains the existing system `pip3 install --no-cache-dir --break-system-packages` path. Uses requirements.txt by default; set `environment.python.requirements` to select other or multiple files. Explicit requirements also activate composition without a registry detect match.
|
|
17
|
+
|
|
18
|
+
The project-manager paths share `/deps/python/.venv`. `VIRTUAL_ENV` is set, and its bin directory comes first in both the image PATH and worker exec PATH. Install failures leave `/deps/python/.nomarmy-<manager>-install-failed` (uv, poetry, or pyproject) instead of failing the image build. Copied manifests and lockfiles are hashed into the image tag. Local/path dependencies and workspace members not included in the root metadata context may require an explicit requirements configuration.
|
|
19
|
+
|
|
20
|
+
## Verification and network
|
|
21
|
+
|
|
22
|
+
The registry proposes `python3 -m unittest discover` as the `quick` profile. Config proposals use test commands found in evidence, or suggest `pytest` for project manifests; review that pytest is declared and appropriate. Network level: `none`. Matched harnesses compose the sandbox image used by workers and verification. Verification profiles are proposals; the registry does not change job profiles or network access.
|
|
23
|
+
|
|
24
|
+
## Requirements and artifacts
|
|
25
|
+
|
|
26
|
+
No additional resource requirements or artifact globs are declared.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Private registries
|
|
30
|
+
|
|
31
|
+
Use `registries:` in the gitignored `.nomarmy.local.yml` only; the committed
|
|
32
|
+
config rejects it. Values are host credential **file paths**, never tokens.
|
|
33
|
+
pip uses `/etc/pip.conf`; for uv or Poetry use `pip: { netrc: ~/.netrc }` and credential-free index/source URLs in project metadata. The secret is mounted only for the root-owned install RUN, including venv installs.
|
|
34
|
+
|
|
35
|
+
Credentials stay on the host and enter Podman only as build-secret mounts;
|
|
36
|
+
no credential files are copied into the context or passed to sandbox jobs.
|
|
37
|
+
Use a **read-only, least-privilege token** and reviewed dependencies. Secrets
|
|
38
|
+
are accessible to install code during that RUN, so malicious packages are not
|
|
39
|
+
made safe by a mount. Credential rotation invalidates both tag and install
|
|
40
|
+
cache. Credentialed install output/build-error details are withheld.
|
|
41
|
+
|
|
42
|
+
See [Private registries](../../docs/your-repo.md#private-registries) for the
|
|
43
|
+
configuration and required manual canary check: inspect `podman history
|
|
44
|
+
--no-trunc`, image configuration, the exported filesystem **and every saved
|
|
45
|
+
image layer**, plus failure logs/job records, for absent secret bytes. Unit
|
|
46
|
+
tests stub Podman; live isolation needs independent security review.
|