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.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. 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
+ }