nemoris 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delivery-ack.js — Delivery acknowledgement layer for the Nemoris interaction layer.
|
|
3
|
+
* Handles criticality classification, guaranteed send pipeline, and pending decision sweeps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Classify a response's criticality based on explicit tags or contextual signals.
|
|
8
|
+
* @param {string} responseText
|
|
9
|
+
* @param {object} context
|
|
10
|
+
* @returns {"info"|"result"|"decision_required"}
|
|
11
|
+
*/
|
|
12
|
+
export function classifyCriticality(responseText, context = {}) {
|
|
13
|
+
// Explicit tag takes highest priority
|
|
14
|
+
const tagMatch = responseText.match(/\[criticality:(info|result|decision_required)\]/);
|
|
15
|
+
if (tagMatch) return tagMatch[1];
|
|
16
|
+
|
|
17
|
+
// Escalation/timeout/error-recovery + question → decision_required
|
|
18
|
+
if (
|
|
19
|
+
(context.isEscalation || context.isTimeout || context.isErrorRecovery) &&
|
|
20
|
+
responseText.trimEnd().endsWith("?")
|
|
21
|
+
) {
|
|
22
|
+
return "decision_required";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Task completion or failure → result
|
|
26
|
+
if (context.taskCompleted || context.taskFailed) {
|
|
27
|
+
return "result";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return "info";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class DeliveryAck {
|
|
34
|
+
constructor(db, envelopeStore) {
|
|
35
|
+
this.db = db;
|
|
36
|
+
this.envelopes = envelopeStore;
|
|
37
|
+
this._ensureSchema();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_ensureSchema() {
|
|
41
|
+
this.db.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS pending_decisions (
|
|
43
|
+
envelope_id TEXT PRIMARY KEY,
|
|
44
|
+
chat_id TEXT NOT NULL,
|
|
45
|
+
delivered_at TEXT NOT NULL,
|
|
46
|
+
timeout_minutes INTEGER NOT NULL DEFAULT 15,
|
|
47
|
+
re_pinged INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
parked INTEGER NOT NULL DEFAULT 0
|
|
49
|
+
);
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns all pending_decisions rows that have not been parked.
|
|
55
|
+
*/
|
|
56
|
+
listPendingDecisions() {
|
|
57
|
+
return this.db.prepare("SELECT * FROM pending_decisions WHERE parked = 0").all();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Send a message with guaranteed delivery, retrying on failure.
|
|
62
|
+
* On success: marks envelope as delivered, optionally inserts into pending_decisions.
|
|
63
|
+
* On all retries exhausted: marks envelope as dead_letter.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} opts
|
|
66
|
+
* @param {string} opts.sourceAgent
|
|
67
|
+
* @param {string} opts.criticality
|
|
68
|
+
* @param {object} opts.payload
|
|
69
|
+
* @param {string} opts.chatId
|
|
70
|
+
* @param {function} opts.sendFn — async (payload) => messageId
|
|
71
|
+
* @param {string|null} [opts.correlationId]
|
|
72
|
+
* @param {number[]} [opts.retryDelays] — delays in ms between attempts
|
|
73
|
+
* @returns {Promise<{envelopeId: string, messageId?: string, delivered: boolean, error?: Error}>}
|
|
74
|
+
*/
|
|
75
|
+
async sendWithGuarantee({
|
|
76
|
+
sourceAgent,
|
|
77
|
+
criticality,
|
|
78
|
+
payload,
|
|
79
|
+
chatId,
|
|
80
|
+
sendFn,
|
|
81
|
+
correlationId = null,
|
|
82
|
+
retryDelays = [2000, 8000, 32000],
|
|
83
|
+
}) {
|
|
84
|
+
const envelope = this.envelopes.create({
|
|
85
|
+
sourceAgent,
|
|
86
|
+
criticality,
|
|
87
|
+
payloadType: "delivery",
|
|
88
|
+
payload,
|
|
89
|
+
correlationId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let lastError = null;
|
|
93
|
+
|
|
94
|
+
for (let attempt = 0; attempt < retryDelays.length; attempt++) {
|
|
95
|
+
if (attempt > 0 && retryDelays[attempt - 1] > 0) {
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt - 1]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const messageId = await sendFn(payload);
|
|
101
|
+
this.envelopes.updateAck(envelope.id, "delivered");
|
|
102
|
+
|
|
103
|
+
if (criticality === "decision_required") {
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
this.db.prepare(`
|
|
106
|
+
INSERT OR REPLACE INTO pending_decisions (envelope_id, chat_id, delivered_at, timeout_minutes, re_pinged, parked)
|
|
107
|
+
VALUES (?, ?, ?, 15, 0, 0)
|
|
108
|
+
`).run(envelope.id, chatId, now);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { envelopeId: envelope.id, messageId, delivered: true };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
lastError = err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// All retries exhausted
|
|
118
|
+
this.envelopes.updateAck(envelope.id, "dead_letter");
|
|
119
|
+
return { envelopeId: envelope.id, delivered: false, error: lastError };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sweep pending decisions: re-ping after timeout, park after double timeout.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} opts
|
|
126
|
+
* @param {Date} [opts.now]
|
|
127
|
+
* @param {function} opts.sendFn — async (message) => void
|
|
128
|
+
* @returns {Promise<{rePinged: number, parked: number}>}
|
|
129
|
+
*/
|
|
130
|
+
async sweepPendingDecisions({ now = new Date(), sendFn }) {
|
|
131
|
+
const rows = this.db.prepare(`
|
|
132
|
+
SELECT pd.*, e.payload
|
|
133
|
+
FROM pending_decisions pd
|
|
134
|
+
JOIN envelopes e ON e.id = pd.envelope_id
|
|
135
|
+
WHERE pd.parked = 0
|
|
136
|
+
`).all();
|
|
137
|
+
|
|
138
|
+
let rePinged = 0;
|
|
139
|
+
let parked = 0;
|
|
140
|
+
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const deliveredAt = new Date(row.delivered_at);
|
|
143
|
+
const elapsedMs = now.getTime() - deliveredAt.getTime();
|
|
144
|
+
const elapsedMinutes = elapsedMs / 60000;
|
|
145
|
+
|
|
146
|
+
if (elapsedMinutes >= row.timeout_minutes * 2 && row.re_pinged === 1) {
|
|
147
|
+
// Park it
|
|
148
|
+
this.db.prepare("UPDATE pending_decisions SET parked = 1 WHERE envelope_id = ?").run(row.envelope_id);
|
|
149
|
+
this.envelopes.updateAck(row.envelope_id, "dead_letter");
|
|
150
|
+
parked++;
|
|
151
|
+
} else if (elapsedMinutes >= row.timeout_minutes && row.re_pinged === 0) {
|
|
152
|
+
// Re-ping
|
|
153
|
+
let payloadText = "";
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(row.payload);
|
|
156
|
+
payloadText = parsed.text || parsed.message || JSON.stringify(parsed);
|
|
157
|
+
} catch {
|
|
158
|
+
payloadText = String(row.payload);
|
|
159
|
+
}
|
|
160
|
+
const excerpt = payloadText.slice(0, 100);
|
|
161
|
+
await sendFn(`Still need your input on this when you get a chance (re: ${excerpt})`);
|
|
162
|
+
this.db.prepare("UPDATE pending_decisions SET re_pinged = 1 WHERE envelope_id = ?").run(row.envelope_id);
|
|
163
|
+
rePinged++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { rePinged, parked };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear pending decisions when the operator sends a message, using a timestamp guard.
|
|
172
|
+
* Marks matching envelopes as "responded" and removes them from pending_decisions.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} chatId
|
|
175
|
+
* @param {string} operatorTimestamp — ISO string; only clears decisions delivered before this
|
|
176
|
+
* @returns {number} count of cleared decisions
|
|
177
|
+
*/
|
|
178
|
+
clearPendingDecisions(chatId, operatorTimestamp) {
|
|
179
|
+
const rows = this.db.prepare(`
|
|
180
|
+
SELECT envelope_id FROM pending_decisions
|
|
181
|
+
WHERE chat_id = ? AND delivered_at < ? AND parked = 0
|
|
182
|
+
`).all(chatId, operatorTimestamp);
|
|
183
|
+
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
this.envelopes.updateAck(row.envelope_id, "responded");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = this.db.prepare(`
|
|
189
|
+
DELETE FROM pending_decisions
|
|
190
|
+
WHERE chat_id = ? AND delivered_at < ? AND parked = 0
|
|
191
|
+
`).run(chatId, operatorTimestamp);
|
|
192
|
+
|
|
193
|
+
return result.changes;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { writeJson } from "../../utils/fs.js";
|
|
3
|
+
|
|
4
|
+
function timestampKey() {
|
|
5
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class LocalFileDeliveryAdapter {
|
|
9
|
+
static adapterId = "local_file";
|
|
10
|
+
|
|
11
|
+
constructor({ stateRoot } = {}) {
|
|
12
|
+
this.stateRoot = stateRoot;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async deliver(notification, context = {}) {
|
|
16
|
+
const target = context.target || notification.target || {};
|
|
17
|
+
const bucket =
|
|
18
|
+
target.mode === "peer_agent"
|
|
19
|
+
? `peer-${target.peerId || "unknown"}`
|
|
20
|
+
: String(target.mode || context.profileName || "operator").replace(/[^a-z0-9_-]+/gi, "-");
|
|
21
|
+
const filePath = path.join(this.stateRoot, "outbox", bucket, `${timestampKey()}.json`);
|
|
22
|
+
const payload = {
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
profile: context.profileName || null,
|
|
25
|
+
target,
|
|
26
|
+
jobId: notification.jobId,
|
|
27
|
+
stage: notification.stage,
|
|
28
|
+
message: notification.message || ""
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await writeJson(filePath, payload);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
status: "stored",
|
|
35
|
+
adapter: LocalFileDeliveryAdapter.adapterId,
|
|
36
|
+
filePath,
|
|
37
|
+
bucket,
|
|
38
|
+
target
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
function readFlag(name) {
|
|
7
|
+
const raw = process.env[name];
|
|
8
|
+
return raw === "1" || raw === "true";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseJson(text) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(String(text || "").trim() || "null");
|
|
14
|
+
} catch {
|
|
15
|
+
return {
|
|
16
|
+
raw: String(text || "")
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class OpenClawCliDeliveryAdapter {
|
|
22
|
+
static adapterId = "openclaw_cli";
|
|
23
|
+
|
|
24
|
+
constructor({ execFileImpl } = {}) {
|
|
25
|
+
this.execFileImpl = execFileImpl || execFileAsync;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async deliver(notification, context = {}) {
|
|
29
|
+
const profile = context.profileConfig || {};
|
|
30
|
+
const dryRun = profile.dryRun === true;
|
|
31
|
+
|
|
32
|
+
if (!dryRun && !readFlag("NEMORIS_ALLOW_GATEWAY_DELIVERY")) {
|
|
33
|
+
throw new Error("Gateway delivery disabled. Set NEMORIS_ALLOW_GATEWAY_DELIVERY=1 to enable live gateway delivery.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const channel = profile.channel;
|
|
37
|
+
const target = profile.chatId || profile.target;
|
|
38
|
+
if (!channel) {
|
|
39
|
+
throw new Error("OpenClaw CLI delivery profile is missing channel.");
|
|
40
|
+
}
|
|
41
|
+
if (!target) {
|
|
42
|
+
throw new Error("OpenClaw CLI delivery profile is missing target/chatId.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const args = [
|
|
46
|
+
"message",
|
|
47
|
+
"send",
|
|
48
|
+
"--channel",
|
|
49
|
+
String(channel),
|
|
50
|
+
"--target",
|
|
51
|
+
String(target),
|
|
52
|
+
"--message",
|
|
53
|
+
String(notification.message || ""),
|
|
54
|
+
"--json"
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (profile.accountId) {
|
|
58
|
+
args.push("--account", String(profile.accountId));
|
|
59
|
+
}
|
|
60
|
+
if (profile.threadId) {
|
|
61
|
+
args.push("--thread-id", String(profile.threadId));
|
|
62
|
+
}
|
|
63
|
+
if (profile.replyTo) {
|
|
64
|
+
args.push("--reply-to", String(profile.replyTo));
|
|
65
|
+
}
|
|
66
|
+
if (profile.silent !== false) {
|
|
67
|
+
args.push("--silent");
|
|
68
|
+
}
|
|
69
|
+
if (dryRun) {
|
|
70
|
+
args.push("--dry-run");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { stdout } = await this.execFileImpl("openclaw", args, {
|
|
74
|
+
timeout: Number(profile.timeoutMs || 10000),
|
|
75
|
+
maxBuffer: 512 * 1024
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
status: dryRun ? "gateway_dry_run" : "gateway_sent",
|
|
80
|
+
adapter: OpenClawCliDeliveryAdapter.adapterId,
|
|
81
|
+
target: {
|
|
82
|
+
mode: channel,
|
|
83
|
+
chatId: target,
|
|
84
|
+
accountId: profile.accountId || null
|
|
85
|
+
},
|
|
86
|
+
profile: context.profileName || null,
|
|
87
|
+
command: {
|
|
88
|
+
bin: "openclaw",
|
|
89
|
+
args
|
|
90
|
+
},
|
|
91
|
+
response: parseJson(stdout)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
function readFlag(name) {
|
|
7
|
+
const raw = process.env[name];
|
|
8
|
+
return raw === "1" || raw === "true";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseJson(text) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(String(text || "").trim() || "null");
|
|
14
|
+
} catch {
|
|
15
|
+
return {
|
|
16
|
+
raw: String(text || "")
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class OpenClawPeerDeliveryAdapter {
|
|
22
|
+
static adapterId = "openclaw_peer";
|
|
23
|
+
|
|
24
|
+
constructor({ execFileImpl } = {}) {
|
|
25
|
+
this.execFileImpl = execFileImpl || execFileAsync;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async deliver(notification, context = {}) {
|
|
29
|
+
const profile = context.profileConfig || {};
|
|
30
|
+
const target = context.target || {};
|
|
31
|
+
const dryRun = profile.dryRun === true;
|
|
32
|
+
|
|
33
|
+
if (!dryRun && !readFlag("NEMORIS_ALLOW_PEER_DELIVERY")) {
|
|
34
|
+
throw new Error("Peer delivery disabled. Set NEMORIS_ALLOW_PEER_DELIVERY=1 to enable live peer delivery.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sessionKey = target.sessionKey || profile.sessionKey;
|
|
38
|
+
if (!sessionKey) {
|
|
39
|
+
throw new Error("Peer delivery requires a sessionKey.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (dryRun) {
|
|
43
|
+
return {
|
|
44
|
+
status: "peer_preview",
|
|
45
|
+
adapter: OpenClawPeerDeliveryAdapter.adapterId,
|
|
46
|
+
profile: context.profileName || null,
|
|
47
|
+
peerId: target.peerId || null,
|
|
48
|
+
sessionKey,
|
|
49
|
+
command: {
|
|
50
|
+
bin: "openclaw",
|
|
51
|
+
args: [
|
|
52
|
+
"agent",
|
|
53
|
+
"--message",
|
|
54
|
+
String(notification.message || ""),
|
|
55
|
+
"--session-id",
|
|
56
|
+
String(sessionKey),
|
|
57
|
+
"--json"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
response: {
|
|
61
|
+
ok: true,
|
|
62
|
+
preview: true
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const args = [
|
|
68
|
+
"agent",
|
|
69
|
+
"--message",
|
|
70
|
+
String(notification.message || ""),
|
|
71
|
+
"--session-id",
|
|
72
|
+
String(sessionKey),
|
|
73
|
+
"--json"
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
if (profile.agentId) {
|
|
77
|
+
args.push("--agent", String(profile.agentId));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { stdout } = await this.execFileImpl("openclaw", args, {
|
|
81
|
+
timeout: Number(profile.timeoutMs || 15000),
|
|
82
|
+
maxBuffer: 1024 * 1024
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
status: dryRun ? "peer_preview" : "peer_sent",
|
|
87
|
+
adapter: OpenClawPeerDeliveryAdapter.adapterId,
|
|
88
|
+
profile: context.profileName || null,
|
|
89
|
+
peerId: target.peerId || null,
|
|
90
|
+
sessionKey,
|
|
91
|
+
command: {
|
|
92
|
+
bin: "openclaw",
|
|
93
|
+
args
|
|
94
|
+
},
|
|
95
|
+
response: parseJson(stdout)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ShadowDeliveryAdapter {
|
|
2
|
+
static adapterId = "shadow";
|
|
3
|
+
|
|
4
|
+
async deliver(notification, context = {}) {
|
|
5
|
+
return {
|
|
6
|
+
status: "shadowed",
|
|
7
|
+
adapter: ShadowDeliveryAdapter.adapterId,
|
|
8
|
+
target: notification.target,
|
|
9
|
+
message: notification.message,
|
|
10
|
+
profile: context.profileName || null
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { OUTBOUND_ADDRESS_POLICY } from "../../security/ssrf-check.js";
|
|
2
|
+
import { fetchWithOutboundPolicy } from "../ssrf.js";
|
|
3
|
+
|
|
4
|
+
function readFlag(name) {
|
|
5
|
+
const raw = process.env[name];
|
|
6
|
+
return raw === "1" || raw === "true";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class StandaloneHttpDeliveryAdapter {
|
|
10
|
+
static adapterId = "standalone_http";
|
|
11
|
+
|
|
12
|
+
constructor({ fetchImpl } = {}) {
|
|
13
|
+
this.fetchImpl = fetchImpl || globalThis.fetch;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async deliver(notification, context = {}) {
|
|
17
|
+
if (!this.fetchImpl) {
|
|
18
|
+
throw new Error("No fetch implementation available for standalone HTTP delivery.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const profile = context.profileConfig || {};
|
|
22
|
+
const dryRun = profile.dryRun === true;
|
|
23
|
+
const baseUrl = String(profile.baseUrl || "").replace(/\/$/, "");
|
|
24
|
+
if (!baseUrl) {
|
|
25
|
+
throw new Error("Standalone HTTP delivery profile is missing baseUrl.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!dryRun && !readFlag("NEMORIS_ALLOW_STANDALONE_HTTP_DELIVERY")) {
|
|
29
|
+
throw new Error("Standalone HTTP delivery disabled. Set NEMORIS_ALLOW_STANDALONE_HTTP_DELIVERY=1 to enable live sends.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const headers = {
|
|
33
|
+
"content-type": "application/json"
|
|
34
|
+
};
|
|
35
|
+
const tokenEnv = profile.authTokenEnv || null;
|
|
36
|
+
const token = tokenEnv ? process.env[tokenEnv] || null : null;
|
|
37
|
+
if (token) {
|
|
38
|
+
headers.authorization = `Bearer ${token}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const payload = {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
jobId: notification.jobId,
|
|
44
|
+
stage: notification.stage,
|
|
45
|
+
status: notification.status || "ready",
|
|
46
|
+
profile: context.profileName || null,
|
|
47
|
+
target: context.target || notification.target || null,
|
|
48
|
+
message: notification.message || ""
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
return {
|
|
53
|
+
status: "http_preview",
|
|
54
|
+
adapter: StandaloneHttpDeliveryAdapter.adapterId,
|
|
55
|
+
endpoint: `${baseUrl}/messages`,
|
|
56
|
+
payload
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Standalone delivery is operator-configured. Allow loopback for the local
|
|
61
|
+
// transport server, but block other private/reserved destinations.
|
|
62
|
+
const response = await fetchWithOutboundPolicy(
|
|
63
|
+
`${baseUrl}/messages`,
|
|
64
|
+
{
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers,
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
signal: typeof AbortSignal?.timeout === "function" ? AbortSignal.timeout(Number(profile.timeoutMs || 5000)) : undefined
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
fetchImpl: this.fetchImpl,
|
|
72
|
+
surface: "standalone_http_delivery",
|
|
73
|
+
addressPolicy: OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
74
|
+
allowLoopbackAddresses: true,
|
|
75
|
+
privateAddressMessage: `Standalone HTTP delivery blocked — target resolves to a private/reserved IP address for ${baseUrl}.`,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
let data;
|
|
80
|
+
try {
|
|
81
|
+
data = text ? JSON.parse(text) : null;
|
|
82
|
+
} catch {
|
|
83
|
+
data = { raw: text };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`Standalone HTTP delivery failed with ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
status: "http_sent",
|
|
92
|
+
adapter: StandaloneHttpDeliveryAdapter.adapterId,
|
|
93
|
+
endpoint: `${baseUrl}/messages`,
|
|
94
|
+
payload,
|
|
95
|
+
response: data
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getDaemonEnv } from "../../onboarding/platform.js";
|
|
2
|
+
|
|
3
|
+
function readFlag(name) {
|
|
4
|
+
const raw = process.env[name];
|
|
5
|
+
return raw === "1" || raw === "true";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function resolveSecretValue(name, { execFileImpl, platform } = {}) {
|
|
9
|
+
return getDaemonEnv(name, { execFileImpl, platform });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class TelegramDeliveryAdapter {
|
|
13
|
+
static adapterId = "telegram";
|
|
14
|
+
|
|
15
|
+
constructor({ fetchImpl, secretResolver, execFileImpl } = {}) {
|
|
16
|
+
this.fetchImpl = fetchImpl || globalThis.fetch;
|
|
17
|
+
this.secretResolver =
|
|
18
|
+
secretResolver ||
|
|
19
|
+
((name) =>
|
|
20
|
+
resolveSecretValue(name, {
|
|
21
|
+
execFileImpl
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
requireFetch() {
|
|
26
|
+
if (!this.fetchImpl) {
|
|
27
|
+
throw new Error("Telegram delivery requires fetch support.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async deliver(notification, context = {}) {
|
|
32
|
+
this.requireFetch();
|
|
33
|
+
|
|
34
|
+
if (!readFlag("NEMORIS_ALLOW_TELEGRAM_DELIVERY")) {
|
|
35
|
+
throw new Error("Telegram delivery disabled. Set NEMORIS_ALLOW_TELEGRAM_DELIVERY=1 to enable live delivery.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const profile = context.profileConfig || {};
|
|
39
|
+
const tokenEnv = profile.botTokenEnv;
|
|
40
|
+
const secret = await this.secretResolver(tokenEnv);
|
|
41
|
+
const token = secret.value;
|
|
42
|
+
if (!token) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Missing Telegram bot token env ${tokenEnv || "(unset profile token env)"} in process env or daemon config.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use notification's chatId for interactive (Telegram-sourced) jobs,
|
|
49
|
+
// fall back to static profile chatId for scheduled job delivery.
|
|
50
|
+
const chatId = (notification.source === "telegram" && notification.chatId)
|
|
51
|
+
? notification.chatId
|
|
52
|
+
: profile.chatId;
|
|
53
|
+
if (!chatId) {
|
|
54
|
+
throw new Error("Telegram delivery profile is missing chatId.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Split long messages and send with delay between chunks
|
|
58
|
+
const { splitTelegramMessage } = await import("../telegram-inbound.js");
|
|
59
|
+
const chunks = splitTelegramMessage(notification.message);
|
|
60
|
+
let lastResponse;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
63
|
+
if (i > 0) {
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 250)); // 250ms delay between chunks
|
|
65
|
+
}
|
|
66
|
+
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
67
|
+
lastResponse = await this.fetchImpl(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"content-type": "application/json"
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
chat_id: chatId,
|
|
74
|
+
text: chunks[i],
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const response = lastResponse;
|
|
80
|
+
|
|
81
|
+
const raw = await response.text();
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(raw);
|
|
85
|
+
} catch {
|
|
86
|
+
parsed = raw;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Telegram delivery failed with status ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
status: "sent",
|
|
95
|
+
adapter: TelegramDeliveryAdapter.adapterId,
|
|
96
|
+
authSource: secret.source,
|
|
97
|
+
target: {
|
|
98
|
+
mode: "telegram",
|
|
99
|
+
chatId
|
|
100
|
+
},
|
|
101
|
+
response: parsed
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|