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,87 +1,87 @@
|
|
|
1
|
-
import { BUILT_IN_RULES, matchRule, isCoolingDown } from "./rules.js";
|
|
2
|
-
|
|
3
|
-
export class HealingTick {
|
|
4
|
-
constructor({ repairLog, ruleStaging, executeAction, notify, queueNurseJob }) {
|
|
5
|
-
this.repairLog = repairLog;
|
|
6
|
-
this.ruleStaging = ruleStaging;
|
|
7
|
-
this.executeAction = executeAction;
|
|
8
|
-
this.notify = notify || (async () => {});
|
|
9
|
-
this.queueNurseJob = queueNurseJob || (async () => {});
|
|
10
|
-
this._running = false;
|
|
11
|
-
this._failureQueue = [];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
isRunning() {
|
|
15
|
-
return this._running;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_getAllRules() {
|
|
19
|
-
const staged = this.ruleStaging.getActiveRules();
|
|
20
|
-
return [...BUILT_IN_RULES, ...staged];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async runTick(failures) {
|
|
24
|
-
if (this._running) return { skipped: true };
|
|
25
|
-
this._running = true;
|
|
26
|
-
try {
|
|
27
|
-
const results = [];
|
|
28
|
-
for (const failure of failures) {
|
|
29
|
-
results.push(await this._processFailure(failure));
|
|
30
|
-
}
|
|
31
|
-
return { skipped: false, results };
|
|
32
|
-
} finally {
|
|
33
|
-
this._running = false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async handleFailure(failure) {
|
|
38
|
-
return this.runTick([failure]);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async _processFailure({ type, context }) {
|
|
42
|
-
const allRules = this._getAllRules();
|
|
43
|
-
const rule = matchRule(type, context, allRules);
|
|
44
|
-
|
|
45
|
-
if (!rule) {
|
|
46
|
-
this.repairLog.write({
|
|
47
|
-
source: "daemon", type,
|
|
48
|
-
context: context ? JSON.stringify(context) : null,
|
|
49
|
-
action: "escalate", result: "escalated",
|
|
50
|
-
severity: "critical", escalated: 1
|
|
51
|
-
});
|
|
52
|
-
await this.queueNurseJob({ type, context });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (isCoolingDown(rule, this.repairLog)) return;
|
|
57
|
-
|
|
58
|
-
const success = await this.executeAction(rule.action, context);
|
|
59
|
-
|
|
60
|
-
this.repairLog.write({
|
|
61
|
-
source: "daemon", type,
|
|
62
|
-
context: context ? JSON.stringify(context) : null,
|
|
63
|
-
action: rule.action,
|
|
64
|
-
result: success ? "resolved" : "unresolved",
|
|
65
|
-
severity: rule.severity
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (rule.severity === "notable" || rule.severity === "critical") {
|
|
69
|
-
await this.notify(rule.severity, `[${type}] Action: ${rule.action}, Result: ${success ? "resolved" : "unresolved"}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
promoteReadyRules() {
|
|
74
|
-
const pending = this.ruleStaging.getPendingAutoPromote();
|
|
75
|
-
for (const rule of pending) {
|
|
76
|
-
this.ruleStaging.promote(rule.id);
|
|
77
|
-
}
|
|
78
|
-
return pending.length;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
runMaintenanceSweep() {
|
|
82
|
-
const expiredRules = this.ruleStaging.expirePending();
|
|
83
|
-
const prunedEntries = this.repairLog.pruneResolved(90);
|
|
84
|
-
const promoted = this.promoteReadyRules();
|
|
85
|
-
return { expiredRules, prunedEntries, promoted };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import { BUILT_IN_RULES, matchRule, isCoolingDown } from "./rules.js";
|
|
2
|
+
|
|
3
|
+
export class HealingTick {
|
|
4
|
+
constructor({ repairLog, ruleStaging, executeAction, notify, queueNurseJob }) {
|
|
5
|
+
this.repairLog = repairLog;
|
|
6
|
+
this.ruleStaging = ruleStaging;
|
|
7
|
+
this.executeAction = executeAction;
|
|
8
|
+
this.notify = notify || (async () => {});
|
|
9
|
+
this.queueNurseJob = queueNurseJob || (async () => {});
|
|
10
|
+
this._running = false;
|
|
11
|
+
this._failureQueue = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
isRunning() {
|
|
15
|
+
return this._running;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_getAllRules() {
|
|
19
|
+
const staged = this.ruleStaging.getActiveRules();
|
|
20
|
+
return [...BUILT_IN_RULES, ...staged];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async runTick(failures) {
|
|
24
|
+
if (this._running) return { skipped: true };
|
|
25
|
+
this._running = true;
|
|
26
|
+
try {
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const failure of failures) {
|
|
29
|
+
results.push(await this._processFailure(failure));
|
|
30
|
+
}
|
|
31
|
+
return { skipped: false, results };
|
|
32
|
+
} finally {
|
|
33
|
+
this._running = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async handleFailure(failure) {
|
|
38
|
+
return this.runTick([failure]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _processFailure({ type, context }) {
|
|
42
|
+
const allRules = this._getAllRules();
|
|
43
|
+
const rule = matchRule(type, context, allRules);
|
|
44
|
+
|
|
45
|
+
if (!rule) {
|
|
46
|
+
this.repairLog.write({
|
|
47
|
+
source: "daemon", type,
|
|
48
|
+
context: context ? JSON.stringify(context) : null,
|
|
49
|
+
action: "escalate", result: "escalated",
|
|
50
|
+
severity: "critical", escalated: 1
|
|
51
|
+
});
|
|
52
|
+
await this.queueNurseJob({ type, context });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isCoolingDown(rule, this.repairLog)) return;
|
|
57
|
+
|
|
58
|
+
const success = await this.executeAction(rule.action, context);
|
|
59
|
+
|
|
60
|
+
this.repairLog.write({
|
|
61
|
+
source: "daemon", type,
|
|
62
|
+
context: context ? JSON.stringify(context) : null,
|
|
63
|
+
action: rule.action,
|
|
64
|
+
result: success ? "resolved" : "unresolved",
|
|
65
|
+
severity: rule.severity
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (rule.severity === "notable" || rule.severity === "critical") {
|
|
69
|
+
await this.notify(rule.severity, `[${type}] Action: ${rule.action}, Result: ${success ? "resolved" : "unresolved"}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
promoteReadyRules() {
|
|
74
|
+
const pending = this.ruleStaging.getPendingAutoPromote();
|
|
75
|
+
for (const rule of pending) {
|
|
76
|
+
this.ruleStaging.promote(rule.id);
|
|
77
|
+
}
|
|
78
|
+
return pending.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
runMaintenanceSweep() {
|
|
82
|
+
const expiredRules = this.ruleStaging.expirePending();
|
|
83
|
+
const prunedEntries = this.repairLog.pruneResolved(90);
|
|
84
|
+
const promoted = this.promoteReadyRules();
|
|
85
|
+
return { expiredRules, prunedEntries, promoted };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import { inspectOutboundUrl, OUTBOUND_ADDRESS_POLICY } from "../security/ssrf-check.js";
|
|
2
|
-
|
|
3
|
-
const MEMORY_BLOAT_ROW_THRESHOLD = 10_000;
|
|
4
|
-
const MEMORY_BLOAT_SIZE_THRESHOLD = 500 * 1024 * 1024; // 500MB
|
|
5
|
-
const EMBEDDING_DRIFT_THRESHOLD = 0.10; // 10%
|
|
6
|
-
|
|
7
|
-
export async function checkProvider(providerId, endpoint, fetchImpl) {
|
|
8
|
-
try {
|
|
9
|
-
// Health probes may hit operator-configured provider endpoints, so enforce
|
|
10
|
-
// the same outbound SSRF policy as live provider traffic.
|
|
11
|
-
const inspection = await inspectOutboundUrl(endpoint, {
|
|
12
|
-
addressPolicy: providerId === "ollama" ? OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK : OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
13
|
-
loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${endpoint}.`,
|
|
14
|
-
privateAddressMessage: `Provider request blocked — target resolves to a private/reserved IP address for ${providerId}.`,
|
|
15
|
-
});
|
|
16
|
-
if (!inspection.ok) {
|
|
17
|
-
return { status: "failure", type: "provider_blocked", provider: providerId, error: inspection.reason };
|
|
18
|
-
}
|
|
19
|
-
const res = await fetchImpl(endpoint, { method: "HEAD", signal: AbortSignal.timeout(5000) });
|
|
20
|
-
if (res.ok) return { status: "ok", provider: providerId };
|
|
21
|
-
if (res.status >= 500) return { status: "failure", type: "provider_5xx", provider: providerId, code: res.status };
|
|
22
|
-
if (res.status === 429) return { status: "failure", type: "provider_rate_limit", provider: providerId, code: 429 };
|
|
23
|
-
if (res.status === 401 || res.status === 403) return { status: "failure", type: "provider_auth", provider: providerId, code: res.status };
|
|
24
|
-
return { status: "failure", type: "provider_client_error", provider: providerId, code: res.status };
|
|
25
|
-
} catch (err) {
|
|
26
|
-
return { status: "failure", type: "provider_timeout", provider: providerId, error: err.message };
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function checkMcp(serverId, client) {
|
|
31
|
-
if (!client || !client.isAlive()) {
|
|
32
|
-
return { status: "failure", type: "mcp_exit", server: serverId };
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const pong = await Promise.race([
|
|
36
|
-
client.ping ? client.ping() : Promise.resolve(true),
|
|
37
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 2000))
|
|
38
|
-
]);
|
|
39
|
-
return { status: "ok", server: serverId };
|
|
40
|
-
} catch {
|
|
41
|
-
return { status: "failure", type: "mcp_timeout", server: serverId };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function checkStaleJobs(stateStore) {
|
|
46
|
-
try {
|
|
47
|
-
const running = stateStore.db.prepare(
|
|
48
|
-
"SELECT job_id FROM interactive_jobs WHERE status = 'running' AND created_at < datetime('now', '-5 minutes')"
|
|
49
|
-
).all();
|
|
50
|
-
if (running.length > 0) {
|
|
51
|
-
return { status: "failure", type: "stale_job", staleCount: running.length, jobIds: running.map(r => r.job_id) };
|
|
52
|
-
}
|
|
53
|
-
return { status: "ok" };
|
|
54
|
-
} catch {
|
|
55
|
-
return { status: "ok" };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function checkDelivery(lastSendResult) {
|
|
60
|
-
if (!lastSendResult) return { status: "ok" };
|
|
61
|
-
if (!lastSendResult.ok) {
|
|
62
|
-
return {
|
|
63
|
-
status: "failure",
|
|
64
|
-
type: "delivery_api_fail",
|
|
65
|
-
error_code: lastSendResult.error_code,
|
|
66
|
-
description: lastSendResult.description
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
if (!lastSendResult.result?.message_id) {
|
|
70
|
-
return { status: "failure", type: "delivery_silent_drop" };
|
|
71
|
-
}
|
|
72
|
-
return { status: "ok", message_id: lastSendResult.result.message_id };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function checkMemoryHealth(db, stats) {
|
|
76
|
-
const { sessionRowCount, dbFileSizeBytes, embeddingIndexCount, embeddingSourceCount } = stats;
|
|
77
|
-
|
|
78
|
-
if (sessionRowCount > MEMORY_BLOAT_ROW_THRESHOLD || dbFileSizeBytes > MEMORY_BLOAT_SIZE_THRESHOLD) {
|
|
79
|
-
return { status: "failure", type: "memory_bloat", sessionRowCount, dbFileSizeBytes };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (embeddingSourceCount > 0) {
|
|
83
|
-
const drift = Math.abs(embeddingIndexCount - embeddingSourceCount) / embeddingSourceCount;
|
|
84
|
-
if (drift > EMBEDDING_DRIFT_THRESHOLD) {
|
|
85
|
-
return { status: "failure", type: "embedding_drift", drift: Math.round(drift * 100), embeddingIndexCount, embeddingSourceCount };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return { status: "ok" };
|
|
90
|
-
}
|
|
1
|
+
import { inspectOutboundUrl, OUTBOUND_ADDRESS_POLICY } from "../security/ssrf-check.js";
|
|
2
|
+
|
|
3
|
+
const MEMORY_BLOAT_ROW_THRESHOLD = 10_000;
|
|
4
|
+
const MEMORY_BLOAT_SIZE_THRESHOLD = 500 * 1024 * 1024; // 500MB
|
|
5
|
+
const EMBEDDING_DRIFT_THRESHOLD = 0.10; // 10%
|
|
6
|
+
|
|
7
|
+
export async function checkProvider(providerId, endpoint, fetchImpl) {
|
|
8
|
+
try {
|
|
9
|
+
// Health probes may hit operator-configured provider endpoints, so enforce
|
|
10
|
+
// the same outbound SSRF policy as live provider traffic.
|
|
11
|
+
const inspection = await inspectOutboundUrl(endpoint, {
|
|
12
|
+
addressPolicy: providerId === "ollama" ? OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK : OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
13
|
+
loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${endpoint}.`,
|
|
14
|
+
privateAddressMessage: `Provider request blocked — target resolves to a private/reserved IP address for ${providerId}.`,
|
|
15
|
+
});
|
|
16
|
+
if (!inspection.ok) {
|
|
17
|
+
return { status: "failure", type: "provider_blocked", provider: providerId, error: inspection.reason };
|
|
18
|
+
}
|
|
19
|
+
const res = await fetchImpl(endpoint, { method: "HEAD", signal: AbortSignal.timeout(5000) });
|
|
20
|
+
if (res.ok) return { status: "ok", provider: providerId };
|
|
21
|
+
if (res.status >= 500) return { status: "failure", type: "provider_5xx", provider: providerId, code: res.status };
|
|
22
|
+
if (res.status === 429) return { status: "failure", type: "provider_rate_limit", provider: providerId, code: 429 };
|
|
23
|
+
if (res.status === 401 || res.status === 403) return { status: "failure", type: "provider_auth", provider: providerId, code: res.status };
|
|
24
|
+
return { status: "failure", type: "provider_client_error", provider: providerId, code: res.status };
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return { status: "failure", type: "provider_timeout", provider: providerId, error: err.message };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function checkMcp(serverId, client) {
|
|
31
|
+
if (!client || !client.isAlive()) {
|
|
32
|
+
return { status: "failure", type: "mcp_exit", server: serverId };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const pong = await Promise.race([
|
|
36
|
+
client.ping ? client.ping() : Promise.resolve(true),
|
|
37
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 2000))
|
|
38
|
+
]);
|
|
39
|
+
return { status: "ok", server: serverId };
|
|
40
|
+
} catch {
|
|
41
|
+
return { status: "failure", type: "mcp_timeout", server: serverId };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function checkStaleJobs(stateStore) {
|
|
46
|
+
try {
|
|
47
|
+
const running = stateStore.db.prepare(
|
|
48
|
+
"SELECT job_id FROM interactive_jobs WHERE status = 'running' AND created_at < datetime('now', '-5 minutes')"
|
|
49
|
+
).all();
|
|
50
|
+
if (running.length > 0) {
|
|
51
|
+
return { status: "failure", type: "stale_job", staleCount: running.length, jobIds: running.map(r => r.job_id) };
|
|
52
|
+
}
|
|
53
|
+
return { status: "ok" };
|
|
54
|
+
} catch {
|
|
55
|
+
return { status: "ok" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function checkDelivery(lastSendResult) {
|
|
60
|
+
if (!lastSendResult) return { status: "ok" };
|
|
61
|
+
if (!lastSendResult.ok) {
|
|
62
|
+
return {
|
|
63
|
+
status: "failure",
|
|
64
|
+
type: "delivery_api_fail",
|
|
65
|
+
error_code: lastSendResult.error_code,
|
|
66
|
+
description: lastSendResult.description
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!lastSendResult.result?.message_id) {
|
|
70
|
+
return { status: "failure", type: "delivery_silent_drop" };
|
|
71
|
+
}
|
|
72
|
+
return { status: "ok", message_id: lastSendResult.result.message_id };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function checkMemoryHealth(db, stats) {
|
|
76
|
+
const { sessionRowCount, dbFileSizeBytes, embeddingIndexCount, embeddingSourceCount } = stats;
|
|
77
|
+
|
|
78
|
+
if (sessionRowCount > MEMORY_BLOAT_ROW_THRESHOLD || dbFileSizeBytes > MEMORY_BLOAT_SIZE_THRESHOLD) {
|
|
79
|
+
return { status: "failure", type: "memory_bloat", sessionRowCount, dbFileSizeBytes };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (embeddingSourceCount > 0) {
|
|
83
|
+
const drift = Math.abs(embeddingIndexCount - embeddingSourceCount) / embeddingSourceCount;
|
|
84
|
+
if (drift > EMBEDDING_DRIFT_THRESHOLD) {
|
|
85
|
+
return { status: "failure", type: "embedding_drift", drift: Math.round(drift * 100), embeddingIndexCount, embeddingSourceCount };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { status: "ok" };
|
|
90
|
+
}
|
package/src/daemon/notifier.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
const TEMPLATES = {
|
|
2
|
-
mcp_exit: {
|
|
3
|
-
resolved: (ctx) => `Your ${capitalize(ctx?.server || "service")} connection dropped. I restarted it, you're good.`,
|
|
4
|
-
unresolved: (ctx, diag) => `I couldn't fix the ${capitalize(ctx?.server || "service")} connection. ${diag ? `Here's what I found: ${diag}` : ""}`,
|
|
5
|
-
},
|
|
6
|
-
mcp_timeout: {
|
|
7
|
-
resolved: (ctx) => `${capitalize(ctx?.server || "Service")} was unresponsive. Restarted it.`,
|
|
8
|
-
unresolved: (ctx, diag) => `${capitalize(ctx?.server || "Service")} keeps hanging. ${diag || "Manual check needed."}`,
|
|
9
|
-
},
|
|
10
|
-
provider_5xx: {
|
|
11
|
-
resolved: (ctx) => `${capitalize(ctx?.provider || "Provider")} returned an error (${ctx?.code || "5xx"}). Switched to fallback.`,
|
|
12
|
-
},
|
|
13
|
-
update_applied: {
|
|
14
|
-
resolved: (ctx) => `Updated ${ctx?.update_target || "system"} to ${ctx?.to_version || "latest"}.`,
|
|
15
|
-
},
|
|
16
|
-
update_held: {
|
|
17
|
-
held: (ctx) => `${packageName(ctx?.update_target || "Package")} ${ctx?.to_version || ""} available (${ctx?.semver_class || "update"}). Reply /approve <id> to update.`,
|
|
18
|
-
},
|
|
19
|
-
update_rollback: {
|
|
20
|
-
resolved: (ctx) => `Tried updating ${ctx?.update_target || "system"} to ${ctx?.to_version || ""}. Failed smoke test, rolled back safely.`,
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const KNOWN_NAMES = {
|
|
25
|
-
github: "GitHub",
|
|
26
|
-
gitlab: "GitLab",
|
|
27
|
-
openai: "OpenAI",
|
|
28
|
-
anthropic: "Anthropic",
|
|
29
|
-
npm: "npm",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Strip registry prefix (e.g. "npm:express" → "express") without changing casing
|
|
33
|
-
function packageName(s) {
|
|
34
|
-
if (!s) return "";
|
|
35
|
-
return s.replace(/^[a-z]+:/, "");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function capitalize(s) {
|
|
39
|
-
if (!s) return "";
|
|
40
|
-
const lower = s.toLowerCase();
|
|
41
|
-
if (KNOWN_NAMES[lower]) return KNOWN_NAMES[lower];
|
|
42
|
-
// Strip common prefixes like "npm:" for display
|
|
43
|
-
const stripped = s.replace(/^[a-z]+:/, "");
|
|
44
|
-
return stripped.charAt(0).toUpperCase() + stripped.slice(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function composePlainEnglish({ type, action, result, context, diagnosis }) {
|
|
48
|
-
const ctx = typeof context === "string" ? JSON.parse(context) : (context || {});
|
|
49
|
-
const template = TEMPLATES[type]?.[result];
|
|
50
|
-
if (template) return template(ctx, diagnosis);
|
|
51
|
-
|
|
52
|
-
// Fallback for unknown types
|
|
53
|
-
if (result === "resolved") return `Fixed: ${type.replace(/_/g, " ")}. Action: ${action?.replace(/_/g, " ") || "auto"}.`;
|
|
54
|
-
if (result === "unresolved") return `I couldn't fix: ${type.replace(/_/g, " ")}. ${diagnosis || "Manual check needed."}`;
|
|
55
|
-
if (result === "held") return `Awaiting approval: ${type.replace(/_/g, " ")}. Reply /approve <id>.`;
|
|
56
|
-
return `[${type}] ${action} → ${result}`;
|
|
57
|
-
}
|
|
1
|
+
const TEMPLATES = {
|
|
2
|
+
mcp_exit: {
|
|
3
|
+
resolved: (ctx) => `Your ${capitalize(ctx?.server || "service")} connection dropped. I restarted it, you're good.`,
|
|
4
|
+
unresolved: (ctx, diag) => `I couldn't fix the ${capitalize(ctx?.server || "service")} connection. ${diag ? `Here's what I found: ${diag}` : ""}`,
|
|
5
|
+
},
|
|
6
|
+
mcp_timeout: {
|
|
7
|
+
resolved: (ctx) => `${capitalize(ctx?.server || "Service")} was unresponsive. Restarted it.`,
|
|
8
|
+
unresolved: (ctx, diag) => `${capitalize(ctx?.server || "Service")} keeps hanging. ${diag || "Manual check needed."}`,
|
|
9
|
+
},
|
|
10
|
+
provider_5xx: {
|
|
11
|
+
resolved: (ctx) => `${capitalize(ctx?.provider || "Provider")} returned an error (${ctx?.code || "5xx"}). Switched to fallback.`,
|
|
12
|
+
},
|
|
13
|
+
update_applied: {
|
|
14
|
+
resolved: (ctx) => `Updated ${ctx?.update_target || "system"} to ${ctx?.to_version || "latest"}.`,
|
|
15
|
+
},
|
|
16
|
+
update_held: {
|
|
17
|
+
held: (ctx) => `${packageName(ctx?.update_target || "Package")} ${ctx?.to_version || ""} available (${ctx?.semver_class || "update"}). Reply /approve <id> to update.`,
|
|
18
|
+
},
|
|
19
|
+
update_rollback: {
|
|
20
|
+
resolved: (ctx) => `Tried updating ${ctx?.update_target || "system"} to ${ctx?.to_version || ""}. Failed smoke test, rolled back safely.`,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const KNOWN_NAMES = {
|
|
25
|
+
github: "GitHub",
|
|
26
|
+
gitlab: "GitLab",
|
|
27
|
+
openai: "OpenAI",
|
|
28
|
+
anthropic: "Anthropic",
|
|
29
|
+
npm: "npm",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Strip registry prefix (e.g. "npm:express" → "express") without changing casing
|
|
33
|
+
function packageName(s) {
|
|
34
|
+
if (!s) return "";
|
|
35
|
+
return s.replace(/^[a-z]+:/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function capitalize(s) {
|
|
39
|
+
if (!s) return "";
|
|
40
|
+
const lower = s.toLowerCase();
|
|
41
|
+
if (KNOWN_NAMES[lower]) return KNOWN_NAMES[lower];
|
|
42
|
+
// Strip common prefixes like "npm:" for display
|
|
43
|
+
const stripped = s.replace(/^[a-z]+:/, "");
|
|
44
|
+
return stripped.charAt(0).toUpperCase() + stripped.slice(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function composePlainEnglish({ type, action, result, context, diagnosis }) {
|
|
48
|
+
const ctx = typeof context === "string" ? JSON.parse(context) : (context || {});
|
|
49
|
+
const template = TEMPLATES[type]?.[result];
|
|
50
|
+
if (template) return template(ctx, diagnosis);
|
|
51
|
+
|
|
52
|
+
// Fallback for unknown types
|
|
53
|
+
if (result === "resolved") return `Fixed: ${type.replace(/_/g, " ")}. Action: ${action?.replace(/_/g, " ") || "auto"}.`;
|
|
54
|
+
if (result === "unresolved") return `I couldn't fix: ${type.replace(/_/g, " ")}. ${diagnosis || "Manual check needed."}`;
|
|
55
|
+
if (result === "held") return `Awaiting approval: ${type.replace(/_/g, " ")}. Reply /approve <id>.`;
|
|
56
|
+
return `[${type}] ${action} → ${result}`;
|
|
57
|
+
}
|