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,214 +1,214 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TOML config schema validation — validates parsed config objects against
|
|
3
|
-
* expected schemas at load time so that missing/malformed fields fail loudly
|
|
4
|
-
* instead of silently degrading at runtime.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { formatRuntimeError, RuntimeError } from "../utils/errors.js";
|
|
8
|
-
|
|
9
|
-
// ── Schema definitions ──────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
const AGENT_SCHEMA = {
|
|
12
|
-
label: "agent",
|
|
13
|
-
required: {
|
|
14
|
-
id: "string",
|
|
15
|
-
primaryLane: "string",
|
|
16
|
-
memoryPolicy: "string",
|
|
17
|
-
toolPolicy: "string"
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const JOB_SCHEMA = {
|
|
22
|
-
label: "job",
|
|
23
|
-
required: {
|
|
24
|
-
id: "string",
|
|
25
|
-
modelLane: "string"
|
|
26
|
-
},
|
|
27
|
-
atLeastOne: [["schedule", "trigger"]]
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const PROVIDER_SCHEMA = {
|
|
31
|
-
label: "provider",
|
|
32
|
-
required: {
|
|
33
|
-
id: "string"
|
|
34
|
-
},
|
|
35
|
-
atLeastOne: [["adapter", "type"]]
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const RUNTIME_SCHEMA = {
|
|
39
|
-
label: "runtime",
|
|
40
|
-
requiredSections: {
|
|
41
|
-
safety: {
|
|
42
|
-
contextTokens: "number"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const ROUTER_SCHEMA = {
|
|
48
|
-
label: "router",
|
|
49
|
-
minLanes: 1
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const SCHEMAS = {
|
|
53
|
-
agent: AGENT_SCHEMA,
|
|
54
|
-
job: JOB_SCHEMA,
|
|
55
|
-
provider: PROVIDER_SCHEMA,
|
|
56
|
-
runtime: RUNTIME_SCHEMA,
|
|
57
|
-
router: ROUTER_SCHEMA
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// ── Validation engine ───────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
export class ConfigSchemaError extends Error {
|
|
63
|
-
constructor(message, errors) {
|
|
64
|
-
super(message);
|
|
65
|
-
this.name = "ConfigSchemaError";
|
|
66
|
-
this.validationErrors = errors;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function typeLabel(value) {
|
|
71
|
-
if (value === null || value === undefined) return "missing";
|
|
72
|
-
if (Array.isArray(value)) return "array";
|
|
73
|
-
return typeof value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function checkType(value, expectedType) {
|
|
77
|
-
if (value === null || value === undefined) return false;
|
|
78
|
-
if (expectedType === "array") return Array.isArray(value);
|
|
79
|
-
return typeof value === expectedType;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Validate a parsed config object against a schema definition.
|
|
84
|
-
*
|
|
85
|
-
* @param {object} config – the parsed (and key-normalised) config object
|
|
86
|
-
* @param {object} schema – one of the SCHEMAS entries
|
|
87
|
-
* @param {string} filePath – file path for error messages
|
|
88
|
-
* @returns {{ ok: boolean, errors: string[] }}
|
|
89
|
-
*/
|
|
90
|
-
export function validateConfig(config, schema, filePath) {
|
|
91
|
-
const errors = [];
|
|
92
|
-
const ctx = filePath || schema.label || "config";
|
|
93
|
-
|
|
94
|
-
if (!config || typeof config !== "object") {
|
|
95
|
-
errors.push(`${ctx}: config is ${typeLabel(config)}, expected an object`);
|
|
96
|
-
return { ok: false, errors };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Check required fields
|
|
100
|
-
if (schema.required) {
|
|
101
|
-
for (const [field, expectedType] of Object.entries(schema.required)) {
|
|
102
|
-
if (!(field in config) || config[field] === undefined || config[field] === null || config[field] === "") {
|
|
103
|
-
errors.push(`${ctx}: missing required field "${field}"`);
|
|
104
|
-
} else if (!checkType(config[field], expectedType)) {
|
|
105
|
-
errors.push(
|
|
106
|
-
`${ctx}: field "${field}" should be ${expectedType}, got ${typeLabel(config[field])}`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check at-least-one groups (e.g. schedule OR trigger)
|
|
113
|
-
if (schema.atLeastOne) {
|
|
114
|
-
for (const group of schema.atLeastOne) {
|
|
115
|
-
const present = group.filter((field) => field in config && config[field] !== undefined && config[field] !== null && config[field] !== "");
|
|
116
|
-
if (present.length === 0) {
|
|
117
|
-
errors.push(`${ctx}: must have at least one of: ${group.join(", ")}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Check required sections (nested objects with typed sub-fields)
|
|
123
|
-
if (schema.requiredSections) {
|
|
124
|
-
for (const [section, subFields] of Object.entries(schema.requiredSections)) {
|
|
125
|
-
if (!(section in config) || !config[section] || typeof config[section] !== "object") {
|
|
126
|
-
errors.push(`${ctx}: missing required section [${section}]`);
|
|
127
|
-
} else {
|
|
128
|
-
for (const [subField, expectedType] of Object.entries(subFields)) {
|
|
129
|
-
const value = config[section][subField];
|
|
130
|
-
if (value === undefined || value === null) {
|
|
131
|
-
errors.push(`${ctx}: missing required field "${section}.${subField}"`);
|
|
132
|
-
} else if (!checkType(value, expectedType)) {
|
|
133
|
-
errors.push(
|
|
134
|
-
`${ctx}: field "${section}.${subField}" should be ${expectedType}, got ${typeLabel(value)}`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Router-specific: must have at least N lane definitions
|
|
143
|
-
if (schema.minLanes !== undefined) {
|
|
144
|
-
const laneCount = Object.keys(config).length;
|
|
145
|
-
if (laneCount < schema.minLanes) {
|
|
146
|
-
errors.push(`${ctx}: must define at least ${schema.minLanes} lane(s), found ${laneCount}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { ok: errors.length === 0, errors };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Validate and throw if invalid — convenience wrapper used by the config loader.
|
|
155
|
-
*/
|
|
156
|
-
export function validateOrThrow(config, schema, filePath) {
|
|
157
|
-
const result = validateConfig(config, schema, filePath);
|
|
158
|
-
if (!result.ok) {
|
|
159
|
-
const runtimeError = new RuntimeError(
|
|
160
|
-
`Config schema validation failed for ${filePath || schema.label}`,
|
|
161
|
-
{ category: "config", context: { filePath, schema: schema.label, errorCount: result.errors.length }, recoverable: false }
|
|
162
|
-
);
|
|
163
|
-
const formatted = formatRuntimeError(runtimeError, { details: result.errors.join("; ") });
|
|
164
|
-
throw new ConfigSchemaError(formatted, result.errors);
|
|
165
|
-
}
|
|
166
|
-
return config;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Validate all config sections in a full loadAll() result.
|
|
171
|
-
* Returns { ok, errors[] } for CLI / test consumption.
|
|
172
|
-
*/
|
|
173
|
-
export function validateAllConfigs(config, rootDir) {
|
|
174
|
-
const allErrors = [];
|
|
175
|
-
|
|
176
|
-
// Agents
|
|
177
|
-
for (const [id, agent] of Object.entries(config.agents || {})) {
|
|
178
|
-
const filePath = agent.__filePath || `${rootDir}/agents/${id}.toml`;
|
|
179
|
-
const result = validateConfig(agent, AGENT_SCHEMA, filePath);
|
|
180
|
-
allErrors.push(...result.errors);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Jobs
|
|
184
|
-
for (const [id, job] of Object.entries(config.jobs || {})) {
|
|
185
|
-
const filePath = job.__filePath || `${rootDir}/jobs/${id}.toml`;
|
|
186
|
-
const result = validateConfig(job, JOB_SCHEMA, filePath);
|
|
187
|
-
allErrors.push(...result.errors);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Providers
|
|
191
|
-
for (const [id, provider] of Object.entries(config.providers || {})) {
|
|
192
|
-
const filePath = provider.__filePath || `${rootDir}/providers/${id}.toml`;
|
|
193
|
-
const result = validateConfig(provider, PROVIDER_SCHEMA, filePath);
|
|
194
|
-
allErrors.push(...result.errors);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Runtime
|
|
198
|
-
if (config.runtime) {
|
|
199
|
-
const result = validateConfig(config.runtime, RUNTIME_SCHEMA, `${rootDir}/runtime.toml`);
|
|
200
|
-
allErrors.push(...result.errors);
|
|
201
|
-
} else {
|
|
202
|
-
allErrors.push(`${rootDir}/runtime.toml: runtime config is missing entirely`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Router
|
|
206
|
-
if (config.router) {
|
|
207
|
-
const result = validateConfig(config.router, ROUTER_SCHEMA, `${rootDir}/router.toml`);
|
|
208
|
-
allErrors.push(...result.errors);
|
|
209
|
-
} else {
|
|
210
|
-
allErrors.push(`${rootDir}/router.toml: router config is missing entirely`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return { ok: allErrors.length === 0, errors: allErrors };
|
|
214
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* TOML config schema validation — validates parsed config objects against
|
|
3
|
+
* expected schemas at load time so that missing/malformed fields fail loudly
|
|
4
|
+
* instead of silently degrading at runtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatRuntimeError, RuntimeError } from "../utils/errors.js";
|
|
8
|
+
|
|
9
|
+
// ── Schema definitions ──────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const AGENT_SCHEMA = {
|
|
12
|
+
label: "agent",
|
|
13
|
+
required: {
|
|
14
|
+
id: "string",
|
|
15
|
+
primaryLane: "string",
|
|
16
|
+
memoryPolicy: "string",
|
|
17
|
+
toolPolicy: "string"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const JOB_SCHEMA = {
|
|
22
|
+
label: "job",
|
|
23
|
+
required: {
|
|
24
|
+
id: "string",
|
|
25
|
+
modelLane: "string"
|
|
26
|
+
},
|
|
27
|
+
atLeastOne: [["schedule", "trigger"]]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PROVIDER_SCHEMA = {
|
|
31
|
+
label: "provider",
|
|
32
|
+
required: {
|
|
33
|
+
id: "string"
|
|
34
|
+
},
|
|
35
|
+
atLeastOne: [["adapter", "type"]]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RUNTIME_SCHEMA = {
|
|
39
|
+
label: "runtime",
|
|
40
|
+
requiredSections: {
|
|
41
|
+
safety: {
|
|
42
|
+
contextTokens: "number"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ROUTER_SCHEMA = {
|
|
48
|
+
label: "router",
|
|
49
|
+
minLanes: 1
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const SCHEMAS = {
|
|
53
|
+
agent: AGENT_SCHEMA,
|
|
54
|
+
job: JOB_SCHEMA,
|
|
55
|
+
provider: PROVIDER_SCHEMA,
|
|
56
|
+
runtime: RUNTIME_SCHEMA,
|
|
57
|
+
router: ROUTER_SCHEMA
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Validation engine ───────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export class ConfigSchemaError extends Error {
|
|
63
|
+
constructor(message, errors) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "ConfigSchemaError";
|
|
66
|
+
this.validationErrors = errors;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function typeLabel(value) {
|
|
71
|
+
if (value === null || value === undefined) return "missing";
|
|
72
|
+
if (Array.isArray(value)) return "array";
|
|
73
|
+
return typeof value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkType(value, expectedType) {
|
|
77
|
+
if (value === null || value === undefined) return false;
|
|
78
|
+
if (expectedType === "array") return Array.isArray(value);
|
|
79
|
+
return typeof value === expectedType;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a parsed config object against a schema definition.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} config – the parsed (and key-normalised) config object
|
|
86
|
+
* @param {object} schema – one of the SCHEMAS entries
|
|
87
|
+
* @param {string} filePath – file path for error messages
|
|
88
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
89
|
+
*/
|
|
90
|
+
export function validateConfig(config, schema, filePath) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const ctx = filePath || schema.label || "config";
|
|
93
|
+
|
|
94
|
+
if (!config || typeof config !== "object") {
|
|
95
|
+
errors.push(`${ctx}: config is ${typeLabel(config)}, expected an object`);
|
|
96
|
+
return { ok: false, errors };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check required fields
|
|
100
|
+
if (schema.required) {
|
|
101
|
+
for (const [field, expectedType] of Object.entries(schema.required)) {
|
|
102
|
+
if (!(field in config) || config[field] === undefined || config[field] === null || config[field] === "") {
|
|
103
|
+
errors.push(`${ctx}: missing required field "${field}"`);
|
|
104
|
+
} else if (!checkType(config[field], expectedType)) {
|
|
105
|
+
errors.push(
|
|
106
|
+
`${ctx}: field "${field}" should be ${expectedType}, got ${typeLabel(config[field])}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check at-least-one groups (e.g. schedule OR trigger)
|
|
113
|
+
if (schema.atLeastOne) {
|
|
114
|
+
for (const group of schema.atLeastOne) {
|
|
115
|
+
const present = group.filter((field) => field in config && config[field] !== undefined && config[field] !== null && config[field] !== "");
|
|
116
|
+
if (present.length === 0) {
|
|
117
|
+
errors.push(`${ctx}: must have at least one of: ${group.join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check required sections (nested objects with typed sub-fields)
|
|
123
|
+
if (schema.requiredSections) {
|
|
124
|
+
for (const [section, subFields] of Object.entries(schema.requiredSections)) {
|
|
125
|
+
if (!(section in config) || !config[section] || typeof config[section] !== "object") {
|
|
126
|
+
errors.push(`${ctx}: missing required section [${section}]`);
|
|
127
|
+
} else {
|
|
128
|
+
for (const [subField, expectedType] of Object.entries(subFields)) {
|
|
129
|
+
const value = config[section][subField];
|
|
130
|
+
if (value === undefined || value === null) {
|
|
131
|
+
errors.push(`${ctx}: missing required field "${section}.${subField}"`);
|
|
132
|
+
} else if (!checkType(value, expectedType)) {
|
|
133
|
+
errors.push(
|
|
134
|
+
`${ctx}: field "${section}.${subField}" should be ${expectedType}, got ${typeLabel(value)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Router-specific: must have at least N lane definitions
|
|
143
|
+
if (schema.minLanes !== undefined) {
|
|
144
|
+
const laneCount = Object.keys(config).length;
|
|
145
|
+
if (laneCount < schema.minLanes) {
|
|
146
|
+
errors.push(`${ctx}: must define at least ${schema.minLanes} lane(s), found ${laneCount}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { ok: errors.length === 0, errors };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate and throw if invalid — convenience wrapper used by the config loader.
|
|
155
|
+
*/
|
|
156
|
+
export function validateOrThrow(config, schema, filePath) {
|
|
157
|
+
const result = validateConfig(config, schema, filePath);
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
const runtimeError = new RuntimeError(
|
|
160
|
+
`Config schema validation failed for ${filePath || schema.label}`,
|
|
161
|
+
{ category: "config", context: { filePath, schema: schema.label, errorCount: result.errors.length }, recoverable: false }
|
|
162
|
+
);
|
|
163
|
+
const formatted = formatRuntimeError(runtimeError, { details: result.errors.join("; ") });
|
|
164
|
+
throw new ConfigSchemaError(formatted, result.errors);
|
|
165
|
+
}
|
|
166
|
+
return config;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate all config sections in a full loadAll() result.
|
|
171
|
+
* Returns { ok, errors[] } for CLI / test consumption.
|
|
172
|
+
*/
|
|
173
|
+
export function validateAllConfigs(config, rootDir) {
|
|
174
|
+
const allErrors = [];
|
|
175
|
+
|
|
176
|
+
// Agents
|
|
177
|
+
for (const [id, agent] of Object.entries(config.agents || {})) {
|
|
178
|
+
const filePath = agent.__filePath || `${rootDir}/agents/${id}.toml`;
|
|
179
|
+
const result = validateConfig(agent, AGENT_SCHEMA, filePath);
|
|
180
|
+
allErrors.push(...result.errors);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Jobs
|
|
184
|
+
for (const [id, job] of Object.entries(config.jobs || {})) {
|
|
185
|
+
const filePath = job.__filePath || `${rootDir}/jobs/${id}.toml`;
|
|
186
|
+
const result = validateConfig(job, JOB_SCHEMA, filePath);
|
|
187
|
+
allErrors.push(...result.errors);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Providers
|
|
191
|
+
for (const [id, provider] of Object.entries(config.providers || {})) {
|
|
192
|
+
const filePath = provider.__filePath || `${rootDir}/providers/${id}.toml`;
|
|
193
|
+
const result = validateConfig(provider, PROVIDER_SCHEMA, filePath);
|
|
194
|
+
allErrors.push(...result.errors);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Runtime
|
|
198
|
+
if (config.runtime) {
|
|
199
|
+
const result = validateConfig(config.runtime, RUNTIME_SCHEMA, `${rootDir}/runtime.toml`);
|
|
200
|
+
allErrors.push(...result.errors);
|
|
201
|
+
} else {
|
|
202
|
+
allErrors.push(`${rootDir}/runtime.toml: runtime config is missing entirely`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Router
|
|
206
|
+
if (config.router) {
|
|
207
|
+
const result = validateConfig(config.router, ROUTER_SCHEMA, `${rootDir}/router.toml`);
|
|
208
|
+
allErrors.push(...result.errors);
|
|
209
|
+
} else {
|
|
210
|
+
allErrors.push(`${rootDir}/router.toml: router config is missing entirely`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { ok: allErrors.length === 0, errors: allErrors };
|
|
214
|
+
}
|
package/src/config/toml-lite.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { parse } from "smol-toml";
|
|
2
|
-
|
|
3
|
-
export function parseToml(source) {
|
|
4
|
-
if (!source || !String(source).trim()) {
|
|
5
|
-
return {};
|
|
6
|
-
}
|
|
7
|
-
return parse(String(source));
|
|
8
|
-
}
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
|
|
3
|
+
export function parseToml(source) {
|
|
4
|
+
if (!source || !String(source).trim()) {
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
return parse(String(source));
|
|
8
|
+
}
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Factory that creates an action executor wired to real system components.
|
|
3
|
-
* Returns an async function: (actionName, context) => boolean (success).
|
|
4
|
-
*/
|
|
5
|
-
export function createActionHandler({ getBreaker, mcpConsumer, stateStore, telegramSendFn, lastFailedMessage }) {
|
|
6
|
-
const handlers = {
|
|
7
|
-
async circuit_break(ctx) {
|
|
8
|
-
const breaker = getBreaker(ctx.provider);
|
|
9
|
-
if (breaker) breaker.recordFailure(ctx.code || 500);
|
|
10
|
-
return true;
|
|
11
|
-
},
|
|
12
|
-
async retry_then_break(ctx) {
|
|
13
|
-
// Retry once, then circuit break
|
|
14
|
-
const breaker = getBreaker(ctx.provider);
|
|
15
|
-
if (breaker) breaker.recordFailure(ctx.code || 408);
|
|
16
|
-
return true;
|
|
17
|
-
},
|
|
18
|
-
async restart(ctx) {
|
|
19
|
-
if (!mcpConsumer) return false;
|
|
20
|
-
await mcpConsumer.ensureRunning(ctx.server);
|
|
21
|
-
return true;
|
|
22
|
-
},
|
|
23
|
-
async kill_restart(ctx) {
|
|
24
|
-
if (!mcpConsumer) return false;
|
|
25
|
-
await mcpConsumer.shutdown(ctx.server);
|
|
26
|
-
await mcpConsumer.ensureRunning(ctx.server);
|
|
27
|
-
return true;
|
|
28
|
-
},
|
|
29
|
-
async compact() {
|
|
30
|
-
// Trigger is handled by existing maintenance cycle — just return true
|
|
31
|
-
return true;
|
|
32
|
-
},
|
|
33
|
-
async rebuild_index() {
|
|
34
|
-
return true; // Existing embedding rebuild runs in maintenance
|
|
35
|
-
},
|
|
36
|
-
async reap() {
|
|
37
|
-
try {
|
|
38
|
-
const { reapOrphanedJobs } = await import("../runtime/orphan-reaper.js");
|
|
39
|
-
reapOrphanedJobs(stateStore);
|
|
40
|
-
} catch {
|
|
41
|
-
// stateStore may be a stub in tests or unavailable — best-effort
|
|
42
|
-
}
|
|
43
|
-
return true;
|
|
44
|
-
},
|
|
45
|
-
async retry_backoff(ctx) {
|
|
46
|
-
if (!telegramSendFn) return false;
|
|
47
|
-
const delays = [1000, 2000, 4000]; // 3 retries with exponential backoff
|
|
48
|
-
for (const delay of delays) {
|
|
49
|
-
await new Promise(r => setTimeout(r, delay));
|
|
50
|
-
const result = await telegramSendFn(lastFailedMessage || "Retry");
|
|
51
|
-
if (result?.ok) return true;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
},
|
|
55
|
-
async resend(ctx) {
|
|
56
|
-
if (!telegramSendFn || !lastFailedMessage) return false;
|
|
57
|
-
const result = await telegramSendFn(lastFailedMessage);
|
|
58
|
-
return !!result?.ok;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return async (actionName, context) => {
|
|
63
|
-
const handler = handlers[actionName];
|
|
64
|
-
if (!handler) return false;
|
|
65
|
-
try {
|
|
66
|
-
return await handler(context || {});
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Factory that creates an action executor wired to real system components.
|
|
3
|
+
* Returns an async function: (actionName, context) => boolean (success).
|
|
4
|
+
*/
|
|
5
|
+
export function createActionHandler({ getBreaker, mcpConsumer, stateStore, telegramSendFn, lastFailedMessage }) {
|
|
6
|
+
const handlers = {
|
|
7
|
+
async circuit_break(ctx) {
|
|
8
|
+
const breaker = getBreaker(ctx.provider);
|
|
9
|
+
if (breaker) breaker.recordFailure(ctx.code || 500);
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
async retry_then_break(ctx) {
|
|
13
|
+
// Retry once, then circuit break
|
|
14
|
+
const breaker = getBreaker(ctx.provider);
|
|
15
|
+
if (breaker) breaker.recordFailure(ctx.code || 408);
|
|
16
|
+
return true;
|
|
17
|
+
},
|
|
18
|
+
async restart(ctx) {
|
|
19
|
+
if (!mcpConsumer) return false;
|
|
20
|
+
await mcpConsumer.ensureRunning(ctx.server);
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
async kill_restart(ctx) {
|
|
24
|
+
if (!mcpConsumer) return false;
|
|
25
|
+
await mcpConsumer.shutdown(ctx.server);
|
|
26
|
+
await mcpConsumer.ensureRunning(ctx.server);
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
async compact() {
|
|
30
|
+
// Trigger is handled by existing maintenance cycle — just return true
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
async rebuild_index() {
|
|
34
|
+
return true; // Existing embedding rebuild runs in maintenance
|
|
35
|
+
},
|
|
36
|
+
async reap() {
|
|
37
|
+
try {
|
|
38
|
+
const { reapOrphanedJobs } = await import("../runtime/orphan-reaper.js");
|
|
39
|
+
reapOrphanedJobs(stateStore);
|
|
40
|
+
} catch {
|
|
41
|
+
// stateStore may be a stub in tests or unavailable — best-effort
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
async retry_backoff(ctx) {
|
|
46
|
+
if (!telegramSendFn) return false;
|
|
47
|
+
const delays = [1000, 2000, 4000]; // 3 retries with exponential backoff
|
|
48
|
+
for (const delay of delays) {
|
|
49
|
+
await new Promise(r => setTimeout(r, delay));
|
|
50
|
+
const result = await telegramSendFn(lastFailedMessage || "Retry");
|
|
51
|
+
if (result?.ok) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
},
|
|
55
|
+
async resend(ctx) {
|
|
56
|
+
if (!telegramSendFn || !lastFailedMessage) return false;
|
|
57
|
+
const result = await telegramSendFn(lastFailedMessage);
|
|
58
|
+
return !!result?.ok;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return async (actionName, context) => {
|
|
63
|
+
const handler = handlers[actionName];
|
|
64
|
+
if (!handler) return false;
|
|
65
|
+
try {
|
|
66
|
+
return await handler(context || {});
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|