nemoris 0.1.0 → 0.1.2
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/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate phase — checks that the scaffolded config directory is well-formed.
|
|
3
|
-
*
|
|
4
|
-
* Loads config files that are present, runs schema validation, checks that
|
|
5
|
-
* state directories exist and are writable, and probes any configured
|
|
6
|
-
* providers via their adapter healthCheck(). A freshly scaffolded directory
|
|
7
|
-
* has no providers and no router.toml (written later by the hatch phase), so
|
|
8
|
-
* those sections are treated as optional at this stage.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from "node:fs";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { readText, listFilesRecursive } from "../../utils/fs.js";
|
|
14
|
-
import { parseToml } from "../../config/toml-lite.js";
|
|
15
|
-
import { validateAllConfigs } from "../../config/schema-validator.js";
|
|
16
|
-
|
|
17
|
-
// State directories that must exist and be writable after scaffold
|
|
18
|
-
const STATE_DIRS = ["state", path.join("state", "memory")];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Normalise TOML snake_case keys to camelCase (mirrors ConfigLoader behaviour).
|
|
22
|
-
* @param {unknown} value
|
|
23
|
-
* @returns {unknown}
|
|
24
|
-
*/
|
|
25
|
-
function normalizeKeys(value) {
|
|
26
|
-
if (Array.isArray(value)) return value.map(normalizeKeys);
|
|
27
|
-
if (!value || typeof value !== "object") return value;
|
|
28
|
-
|
|
29
|
-
const out = {};
|
|
30
|
-
for (const [k, v] of Object.entries(value)) {
|
|
31
|
-
const norm = k.replace(/_([a-z])/g, (_, l) => l.toUpperCase());
|
|
32
|
-
out[norm] = normalizeKeys(v);
|
|
33
|
-
}
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Load all TOML files in a directory into an id-keyed map.
|
|
39
|
-
* Returns an empty object if the directory is absent.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} dirPath
|
|
42
|
-
* @returns {Promise<Record<string, object>>}
|
|
43
|
-
*/
|
|
44
|
-
async function loadDirAsMap(dirPath) {
|
|
45
|
-
if (!fs.existsSync(dirPath)) return {};
|
|
46
|
-
|
|
47
|
-
const files = (await listFilesRecursive(dirPath)).filter((f) => f.endsWith(".toml"));
|
|
48
|
-
const entries = [];
|
|
49
|
-
for (const filePath of files) {
|
|
50
|
-
const parsed = normalizeKeys(parseToml(await readText(filePath, "")));
|
|
51
|
-
const id = parsed.id || path.basename(filePath, ".toml");
|
|
52
|
-
entries.push([id, { ...parsed, __filePath: filePath }]);
|
|
53
|
-
}
|
|
54
|
-
return Object.fromEntries(entries);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check that state directories exist and are writable.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} installDir
|
|
61
|
-
* @returns {{ ready: boolean, missing: string[] }}
|
|
62
|
-
*/
|
|
63
|
-
function checkStateDirs(installDir) {
|
|
64
|
-
const missing = [];
|
|
65
|
-
for (const rel of STATE_DIRS) {
|
|
66
|
-
const absPath = path.join(installDir, rel);
|
|
67
|
-
if (!fs.existsSync(absPath)) {
|
|
68
|
-
missing.push(rel);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
fs.accessSync(absPath, fs.constants.W_OK);
|
|
73
|
-
} catch {
|
|
74
|
-
missing.push(rel);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { ready: missing.length === 0, missing };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Probe a single provider via its adapter's healthCheck(), if available.
|
|
82
|
-
*
|
|
83
|
-
* @param {string} id
|
|
84
|
-
* @param {object} providerConfig
|
|
85
|
-
* @returns {Promise<{ id: string, healthy: boolean, error?: string }>}
|
|
86
|
-
*/
|
|
87
|
-
async function probeProvider(id, providerConfig) {
|
|
88
|
-
const adapterName = providerConfig.adapter || providerConfig.type;
|
|
89
|
-
if (!adapterName) {
|
|
90
|
-
return { id, healthy: false, error: "no adapter or type configured" };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
// Attempt to dynamically load the adapter module
|
|
95
|
-
const adapterPath = new URL(
|
|
96
|
-
`../../providers/adapters/${adapterName}.js`,
|
|
97
|
-
import.meta.url
|
|
98
|
-
);
|
|
99
|
-
const mod = await import(adapterPath.href);
|
|
100
|
-
const AdapterClass = mod.default || mod[Object.keys(mod)[0]];
|
|
101
|
-
|
|
102
|
-
if (typeof AdapterClass?.prototype?.healthCheck === "function") {
|
|
103
|
-
const instance = new AdapterClass(providerConfig);
|
|
104
|
-
await instance.healthCheck();
|
|
105
|
-
return { id, healthy: true };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Static healthCheck
|
|
109
|
-
if (typeof AdapterClass?.healthCheck === "function") {
|
|
110
|
-
await AdapterClass.healthCheck(providerConfig);
|
|
111
|
-
return { id, healthy: true };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// No healthCheck available — treat as healthy (unchecked)
|
|
115
|
-
return { id, healthy: true };
|
|
116
|
-
} catch (err) {
|
|
117
|
-
return { id, healthy: false, error: err.message };
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Validate a scaffolded nemoris installation directory.
|
|
123
|
-
*
|
|
124
|
-
* @param {string} installDir Absolute path to the installation root.
|
|
125
|
-
* @returns {Promise<{
|
|
126
|
-
* configValid: boolean,
|
|
127
|
-
* errors: string[],
|
|
128
|
-
* stateDirsReady: boolean,
|
|
129
|
-
* providerHealth: Array<{ id: string, healthy: boolean, error?: string }>,
|
|
130
|
-
* }>}
|
|
131
|
-
*/
|
|
132
|
-
export async function validateScaffold(installDir) {
|
|
133
|
-
const configDir = path.join(installDir, "config");
|
|
134
|
-
|
|
135
|
-
// ── 1. Load config sections that are present ──────────────────────────────
|
|
136
|
-
|
|
137
|
-
const [agents, jobs, providers] = await Promise.all([
|
|
138
|
-
loadDirAsMap(path.join(configDir, "agents")),
|
|
139
|
-
loadDirAsMap(path.join(configDir, "jobs")),
|
|
140
|
-
loadDirAsMap(path.join(configDir, "providers")),
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
|
-
// Runtime — required by schema validator; will produce errors if missing
|
|
144
|
-
const runtimeRaw = normalizeKeys(
|
|
145
|
-
parseToml(await readText(path.join(configDir, "runtime.toml"), ""))
|
|
146
|
-
);
|
|
147
|
-
const runtime = Object.keys(runtimeRaw).length > 0 ? runtimeRaw : null;
|
|
148
|
-
|
|
149
|
-
// Router — optional at scaffold stage (written during hatch phase)
|
|
150
|
-
const routerFilePath = path.join(configDir, "router.toml");
|
|
151
|
-
let router = null;
|
|
152
|
-
if (fs.existsSync(routerFilePath)) {
|
|
153
|
-
const raw = parseToml(await readText(routerFilePath, ""));
|
|
154
|
-
const lanes = {};
|
|
155
|
-
for (const [laneName, value] of Object.entries(raw.lanes || {})) {
|
|
156
|
-
lanes[laneName] = {
|
|
157
|
-
primary: value.primary,
|
|
158
|
-
fallback: value.fallback,
|
|
159
|
-
manualBump: value.manual_bump,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
if (Object.keys(lanes).length > 0) {
|
|
163
|
-
router = lanes;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── 2. Build a minimal config object and run schema validation ────────────
|
|
168
|
-
|
|
169
|
-
const config = {
|
|
170
|
-
agents,
|
|
171
|
-
jobs,
|
|
172
|
-
providers,
|
|
173
|
-
...(runtime ? { runtime } : {}),
|
|
174
|
-
...(router ? { router } : {}),
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const validationResult = validateAllConfigs(config, configDir);
|
|
178
|
-
|
|
179
|
-
// router.toml is written during the hatch phase (after providers are
|
|
180
|
-
// configured). At scaffold+identity stage it is not yet present, so we
|
|
181
|
-
// treat the router-missing error as non-fatal when no router is loaded.
|
|
182
|
-
const errors = router
|
|
183
|
-
? validationResult.errors
|
|
184
|
-
: validationResult.errors.filter((e) => !e.includes("router config is missing entirely"));
|
|
185
|
-
|
|
186
|
-
const ok = errors.length === 0;
|
|
187
|
-
|
|
188
|
-
// ── 3. Check state directories ────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
const { ready: stateDirsReady } = checkStateDirs(installDir);
|
|
191
|
-
|
|
192
|
-
// ── 4. Probe configured providers ─────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
const providerHealth = await Promise.all(
|
|
195
|
-
Object.entries(providers).map(([id, cfg]) => probeProvider(id, cfg))
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
configValid: ok,
|
|
200
|
-
errors,
|
|
201
|
-
stateDirsReady,
|
|
202
|
-
providerHealth,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Validate phase — checks that the scaffolded config directory is well-formed.
|
|
3
|
+
*
|
|
4
|
+
* Loads config files that are present, runs schema validation, checks that
|
|
5
|
+
* state directories exist and are writable, and probes any configured
|
|
6
|
+
* providers via their adapter healthCheck(). A freshly scaffolded directory
|
|
7
|
+
* has no providers and no router.toml (written later by the hatch phase), so
|
|
8
|
+
* those sections are treated as optional at this stage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { readText, listFilesRecursive } from "../../utils/fs.js";
|
|
14
|
+
import { parseToml } from "../../config/toml-lite.js";
|
|
15
|
+
import { validateAllConfigs } from "../../config/schema-validator.js";
|
|
16
|
+
|
|
17
|
+
// State directories that must exist and be writable after scaffold
|
|
18
|
+
const STATE_DIRS = ["state", path.join("state", "memory")];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalise TOML snake_case keys to camelCase (mirrors ConfigLoader behaviour).
|
|
22
|
+
* @param {unknown} value
|
|
23
|
+
* @returns {unknown}
|
|
24
|
+
*/
|
|
25
|
+
function normalizeKeys(value) {
|
|
26
|
+
if (Array.isArray(value)) return value.map(normalizeKeys);
|
|
27
|
+
if (!value || typeof value !== "object") return value;
|
|
28
|
+
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [k, v] of Object.entries(value)) {
|
|
31
|
+
const norm = k.replace(/_([a-z])/g, (_, l) => l.toUpperCase());
|
|
32
|
+
out[norm] = normalizeKeys(v);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load all TOML files in a directory into an id-keyed map.
|
|
39
|
+
* Returns an empty object if the directory is absent.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} dirPath
|
|
42
|
+
* @returns {Promise<Record<string, object>>}
|
|
43
|
+
*/
|
|
44
|
+
async function loadDirAsMap(dirPath) {
|
|
45
|
+
if (!fs.existsSync(dirPath)) return {};
|
|
46
|
+
|
|
47
|
+
const files = (await listFilesRecursive(dirPath)).filter((f) => f.endsWith(".toml"));
|
|
48
|
+
const entries = [];
|
|
49
|
+
for (const filePath of files) {
|
|
50
|
+
const parsed = normalizeKeys(parseToml(await readText(filePath, "")));
|
|
51
|
+
const id = parsed.id || path.basename(filePath, ".toml");
|
|
52
|
+
entries.push([id, { ...parsed, __filePath: filePath }]);
|
|
53
|
+
}
|
|
54
|
+
return Object.fromEntries(entries);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check that state directories exist and are writable.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} installDir
|
|
61
|
+
* @returns {{ ready: boolean, missing: string[] }}
|
|
62
|
+
*/
|
|
63
|
+
function checkStateDirs(installDir) {
|
|
64
|
+
const missing = [];
|
|
65
|
+
for (const rel of STATE_DIRS) {
|
|
66
|
+
const absPath = path.join(installDir, rel);
|
|
67
|
+
if (!fs.existsSync(absPath)) {
|
|
68
|
+
missing.push(rel);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
fs.accessSync(absPath, fs.constants.W_OK);
|
|
73
|
+
} catch {
|
|
74
|
+
missing.push(rel);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { ready: missing.length === 0, missing };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Probe a single provider via its adapter's healthCheck(), if available.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} id
|
|
84
|
+
* @param {object} providerConfig
|
|
85
|
+
* @returns {Promise<{ id: string, healthy: boolean, error?: string }>}
|
|
86
|
+
*/
|
|
87
|
+
async function probeProvider(id, providerConfig) {
|
|
88
|
+
const adapterName = providerConfig.adapter || providerConfig.type;
|
|
89
|
+
if (!adapterName) {
|
|
90
|
+
return { id, healthy: false, error: "no adapter or type configured" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Attempt to dynamically load the adapter module
|
|
95
|
+
const adapterPath = new URL(
|
|
96
|
+
`../../providers/adapters/${adapterName}.js`,
|
|
97
|
+
import.meta.url
|
|
98
|
+
);
|
|
99
|
+
const mod = await import(adapterPath.href);
|
|
100
|
+
const AdapterClass = mod.default || mod[Object.keys(mod)[0]];
|
|
101
|
+
|
|
102
|
+
if (typeof AdapterClass?.prototype?.healthCheck === "function") {
|
|
103
|
+
const instance = new AdapterClass(providerConfig);
|
|
104
|
+
await instance.healthCheck();
|
|
105
|
+
return { id, healthy: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Static healthCheck
|
|
109
|
+
if (typeof AdapterClass?.healthCheck === "function") {
|
|
110
|
+
await AdapterClass.healthCheck(providerConfig);
|
|
111
|
+
return { id, healthy: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// No healthCheck available — treat as healthy (unchecked)
|
|
115
|
+
return { id, healthy: true };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { id, healthy: false, error: err.message };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate a scaffolded nemoris installation directory.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} installDir Absolute path to the installation root.
|
|
125
|
+
* @returns {Promise<{
|
|
126
|
+
* configValid: boolean,
|
|
127
|
+
* errors: string[],
|
|
128
|
+
* stateDirsReady: boolean,
|
|
129
|
+
* providerHealth: Array<{ id: string, healthy: boolean, error?: string }>,
|
|
130
|
+
* }>}
|
|
131
|
+
*/
|
|
132
|
+
export async function validateScaffold(installDir) {
|
|
133
|
+
const configDir = path.join(installDir, "config");
|
|
134
|
+
|
|
135
|
+
// ── 1. Load config sections that are present ──────────────────────────────
|
|
136
|
+
|
|
137
|
+
const [agents, jobs, providers] = await Promise.all([
|
|
138
|
+
loadDirAsMap(path.join(configDir, "agents")),
|
|
139
|
+
loadDirAsMap(path.join(configDir, "jobs")),
|
|
140
|
+
loadDirAsMap(path.join(configDir, "providers")),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
// Runtime — required by schema validator; will produce errors if missing
|
|
144
|
+
const runtimeRaw = normalizeKeys(
|
|
145
|
+
parseToml(await readText(path.join(configDir, "runtime.toml"), ""))
|
|
146
|
+
);
|
|
147
|
+
const runtime = Object.keys(runtimeRaw).length > 0 ? runtimeRaw : null;
|
|
148
|
+
|
|
149
|
+
// Router — optional at scaffold stage (written during hatch phase)
|
|
150
|
+
const routerFilePath = path.join(configDir, "router.toml");
|
|
151
|
+
let router = null;
|
|
152
|
+
if (fs.existsSync(routerFilePath)) {
|
|
153
|
+
const raw = parseToml(await readText(routerFilePath, ""));
|
|
154
|
+
const lanes = {};
|
|
155
|
+
for (const [laneName, value] of Object.entries(raw.lanes || {})) {
|
|
156
|
+
lanes[laneName] = {
|
|
157
|
+
primary: value.primary,
|
|
158
|
+
fallback: value.fallback,
|
|
159
|
+
manualBump: value.manual_bump,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (Object.keys(lanes).length > 0) {
|
|
163
|
+
router = lanes;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── 2. Build a minimal config object and run schema validation ────────────
|
|
168
|
+
|
|
169
|
+
const config = {
|
|
170
|
+
agents,
|
|
171
|
+
jobs,
|
|
172
|
+
providers,
|
|
173
|
+
...(runtime ? { runtime } : {}),
|
|
174
|
+
...(router ? { router } : {}),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const validationResult = validateAllConfigs(config, configDir);
|
|
178
|
+
|
|
179
|
+
// router.toml is written during the hatch phase (after providers are
|
|
180
|
+
// configured). At scaffold+identity stage it is not yet present, so we
|
|
181
|
+
// treat the router-missing error as non-fatal when no router is loaded.
|
|
182
|
+
const errors = router
|
|
183
|
+
? validationResult.errors
|
|
184
|
+
: validationResult.errors.filter((e) => !e.includes("router config is missing entirely"));
|
|
185
|
+
|
|
186
|
+
const ok = errors.length === 0;
|
|
187
|
+
|
|
188
|
+
// ── 3. Check state directories ────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
const { ready: stateDirsReady } = checkStateDirs(installDir);
|
|
191
|
+
|
|
192
|
+
// ── 4. Probe configured providers ─────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
const providerHealth = await Promise.all(
|
|
195
|
+
Object.entries(providers).map(([id, cfg]) => probeProvider(id, cfg))
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
configValid: ok,
|
|
200
|
+
errors,
|
|
201
|
+
stateDirsReady,
|
|
202
|
+
providerHealth,
|
|
203
|
+
};
|
|
204
|
+
}
|