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,134 @@
1
+ import { estimateInvocationTokens } from "./token-estimator.js";
2
+
3
+ let jsTiktokenModulePromise = null;
4
+
5
+ function normalizeModelId(modelId) {
6
+ return String(modelId || "").toLowerCase();
7
+ }
8
+
9
+ function extractOpenAiTokenizerModel(modelId) {
10
+ const normalized = normalizeModelId(modelId);
11
+ const candidates = [
12
+ "gpt-4.1-mini",
13
+ "gpt-4.1",
14
+ "gpt-4o-mini",
15
+ "gpt-4o",
16
+ "o3-mini",
17
+ "o3",
18
+ "o1-mini",
19
+ "o1",
20
+ "gpt-4-turbo",
21
+ "gpt-4",
22
+ "gpt-3.5-turbo"
23
+ ];
24
+
25
+ for (const candidate of candidates) {
26
+ if (normalized.includes(candidate)) {
27
+ return candidate;
28
+ }
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ function renderInvocationText(invocation) {
35
+ const sections = [];
36
+ if (invocation?.system) {
37
+ sections.push(`system\n${invocation.system}`);
38
+ }
39
+ for (const message of invocation?.messages || []) {
40
+ sections.push(`${message.role || "user"}\n${message.content || ""}`);
41
+ }
42
+ return sections.join("\n\n");
43
+ }
44
+
45
+ async function loadJsTiktoken() {
46
+ if (!jsTiktokenModulePromise) {
47
+ jsTiktokenModulePromise = import("js-tiktoken").catch(() => null);
48
+ }
49
+ return jsTiktokenModulePromise;
50
+ }
51
+
52
+ function countWithApproximateTokenizer(modelName, invocation) {
53
+ const rendered = renderInvocationText(invocation);
54
+ const textTokens = Math.max(1, Math.ceil(rendered.length / 4));
55
+ const framing = invocation?.messages?.length ? invocation.messages.length * 6 : 0;
56
+ return {
57
+ total: textTokens + framing,
58
+ mode: "model_specific",
59
+ source: `js-tiktoken:${modelName}`,
60
+ details: {
61
+ textTokens,
62
+ framing,
63
+ fallback: "approximate_model_specific",
64
+ }
65
+ };
66
+ }
67
+
68
+ async function countWithJsTiktoken(modelName, invocation) {
69
+ const jsTiktoken = await loadJsTiktoken();
70
+ if (!jsTiktoken?.encodingForModel) {
71
+ return countWithApproximateTokenizer(modelName, invocation);
72
+ }
73
+ const encoder = jsTiktoken.encodingForModel(modelName);
74
+ const rendered = renderInvocationText(invocation);
75
+ const textTokens = encoder.encode(rendered).length;
76
+ const framing = invocation?.messages?.length ? invocation.messages.length * 6 : 0;
77
+ return {
78
+ total: textTokens + framing,
79
+ mode: "model_specific",
80
+ source: `js-tiktoken:${modelName}`,
81
+ details: {
82
+ textTokens,
83
+ framing
84
+ }
85
+ };
86
+ }
87
+
88
+ export class TokenCounter {
89
+ async countInvocation({ providerId, invocation, adapter }) {
90
+ if (adapter && typeof adapter.countTokens === "function") {
91
+ try {
92
+ const total = await adapter.countTokens(invocation);
93
+ if (Number.isFinite(total) && total > 0) {
94
+ return {
95
+ total,
96
+ mode: "exact",
97
+ source: `${providerId}:count_tokens`,
98
+ details: {
99
+ providerId
100
+ }
101
+ };
102
+ }
103
+ } catch (error) {
104
+ const fallback = this.countHeuristic(invocation);
105
+ return {
106
+ ...fallback,
107
+ details: {
108
+ ...fallback.details,
109
+ fallbackReason: error.message
110
+ }
111
+ };
112
+ }
113
+ }
114
+
115
+ const tokenizerModel = extractOpenAiTokenizerModel(invocation?.model);
116
+ if (tokenizerModel) {
117
+ try {
118
+ return await countWithJsTiktoken(tokenizerModel, invocation);
119
+ } catch {}
120
+ }
121
+
122
+ return this.countHeuristic(invocation);
123
+ }
124
+
125
+ countHeuristic(invocation) {
126
+ const estimate = estimateInvocationTokens(invocation);
127
+ return {
128
+ total: estimate.total,
129
+ mode: "heuristic",
130
+ source: "multi_strategy_estimator",
131
+ details: estimate
132
+ };
133
+ }
134
+ }
@@ -0,0 +1,59 @@
1
+ function countMatches(text, pattern) {
2
+ return (text.match(pattern) || []).length;
3
+ }
4
+
5
+ export function estimateTextTokens(text) {
6
+ const raw = String(text || "");
7
+ if (!raw) {
8
+ return {
9
+ total: 0,
10
+ strategies: {
11
+ charBased: 0,
12
+ wordBased: 0,
13
+ denseText: 0,
14
+ cjkAware: 0
15
+ }
16
+ };
17
+ }
18
+
19
+ const words = raw.trim().split(/\s+/).filter(Boolean).length;
20
+ const punctuation = countMatches(raw, /[{}[\]():;,.]/g);
21
+ const operators = countMatches(raw, /[=+\-/*<>|&]/g);
22
+ const quotes = countMatches(raw, /["'`]/g);
23
+ const newlines = countMatches(raw, /\n/g);
24
+ const cjk = countMatches(raw, /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
25
+ const symbolRatio = raw.length ? (punctuation + operators + quotes) / raw.length : 0;
26
+
27
+ const charBased = Math.ceil(raw.length / 4);
28
+ const wordBased = Math.ceil(words * 1.28 + punctuation * 0.18 + operators * 0.2 + newlines * 0.12);
29
+ const denseDivisor = symbolRatio >= 0.12 ? 3.1 : 3.5;
30
+ const denseText = Math.ceil(raw.length / denseDivisor + quotes * 0.08);
31
+ const cjkAware = cjk ? Math.ceil(cjk * 1.15 + (raw.length - cjk) / 4.4) : 0;
32
+
33
+ return {
34
+ total: Math.max(charBased, wordBased, denseText, cjkAware),
35
+ strategies: {
36
+ charBased,
37
+ wordBased,
38
+ denseText,
39
+ cjkAware
40
+ }
41
+ };
42
+ }
43
+
44
+ export function estimateInvocationTokens(invocation) {
45
+ const system = estimateTextTokens(invocation?.system || "");
46
+ const messages = (invocation?.messages || []).map((message) => ({
47
+ role: message.role || "user",
48
+ ...estimateTextTokens(message.content || "")
49
+ }));
50
+ const messageTotal = messages.reduce((sum, message) => sum + message.total, 0);
51
+ const framing = invocation?.messages?.length ? invocation.messages.length * 6 : 0;
52
+
53
+ return {
54
+ total: system.total + messageTotal + framing,
55
+ system,
56
+ messages,
57
+ framing
58
+ };
59
+ }
@@ -0,0 +1,200 @@
1
+ import { ExecApprovalGate } from "./exec-approvals.js";
2
+
3
+ export function parseToolUseBlocks(response) {
4
+ if (!response?.content || !Array.isArray(response.content)) return [];
5
+ return response.content
6
+ .filter((block) => block.type === "tool_use")
7
+ .map((block) => ({ id: block.id, name: block.name, input: block.input || {} }));
8
+ }
9
+
10
+ export function formatToolResults(results) {
11
+ return [{
12
+ role: "user",
13
+ content: results.map((result) => ({
14
+ type: "tool_result",
15
+ tool_use_id: result.toolUseId,
16
+ content: result.output,
17
+ ...(result.isError ? { is_error: true } : {}),
18
+ })),
19
+ }];
20
+ }
21
+
22
+ export function buildPendingApprovalOutput(results) {
23
+ if (results.length === 1) {
24
+ return results[0].output;
25
+ }
26
+ return [
27
+ "Multiple tool calls are waiting for approval before I can continue:",
28
+ ...results.map((result) => `- ${result.output}`),
29
+ ].join("\n");
30
+ }
31
+
32
+ export async function executeToolCalls(toolCalls, registry, policy, agentConfig = null, options = {}) {
33
+ const allowed = new Set(policy.allowed || []);
34
+ const blocked = new Set(policy.blocked || []);
35
+ const approvalGate = options.approvalGate || new ExecApprovalGate({ sendFn: async () => {} });
36
+
37
+ const promises = toolCalls.map(async (call) => {
38
+ if (policy.default === "deny" && !allowed.has(call.name)) {
39
+ return {
40
+ toolUseId: call.id,
41
+ name: call.name,
42
+ args: call.input,
43
+ output: `Tool '${call.name}' is not allowed by agent policy.`,
44
+ isError: true,
45
+ };
46
+ }
47
+ if (blocked.has(call.name)) {
48
+ return {
49
+ toolUseId: call.id,
50
+ name: call.name,
51
+ args: call.input,
52
+ output: `Tool '${call.name}' is blocked by agent policy.`,
53
+ isError: true,
54
+ };
55
+ }
56
+
57
+ if (approvalGate.requiresApproval(call.name, call.input, agentConfig)) {
58
+ const approvalRequest = approvalGate.requestApproval({
59
+ toolName: call.name,
60
+ toolInput: call.input,
61
+ jobId: options.jobId || "unknown",
62
+ agentId: options.agentId || agentConfig?.id || "unknown",
63
+ skipNotify: options.skipApprovalNotification === true,
64
+ });
65
+ const approvalId = approvalRequest?.approvalId || null;
66
+ void approvalRequest.catch(() => {});
67
+ return {
68
+ toolUseId: call.id,
69
+ name: call.name,
70
+ args: call.input,
71
+ output: approvalId
72
+ ? `Approval required for ${call.name}. Awaiting /exec_approve ${approvalId} or /exec_deny ${approvalId}.`
73
+ : `Approval required for ${call.name}. Use /exec_approve or /exec_deny.`,
74
+ isError: false,
75
+ pendingApproval: true,
76
+ approvalId,
77
+ };
78
+ }
79
+
80
+ const result = await registry.executeTool(call.name, call.input);
81
+ if (result.error) {
82
+ return {
83
+ toolUseId: call.id,
84
+ name: call.name,
85
+ args: call.input,
86
+ output: result.error,
87
+ isError: true,
88
+ };
89
+ }
90
+ return {
91
+ toolUseId: call.id,
92
+ name: call.name,
93
+ args: call.input,
94
+ output: result.output,
95
+ isError: false,
96
+ };
97
+ });
98
+
99
+ return Promise.allSettled(promises).then((settled) =>
100
+ settled.map((entry) => entry.status === "fulfilled"
101
+ ? entry.value
102
+ : {
103
+ toolUseId: "unknown",
104
+ name: "unknown",
105
+ args: {},
106
+ output: `Internal error: ${entry.reason}`,
107
+ isError: true,
108
+ })
109
+ );
110
+ }
111
+
112
+ export async function runToolLoop({
113
+ initialResponse,
114
+ adapter,
115
+ invocation,
116
+ registry,
117
+ policy,
118
+ agentConfig = null,
119
+ approvalGate = null,
120
+ options = {}
121
+ }) {
122
+ if (!options.enabled) {
123
+ return {
124
+ finalResponse: initialResponse,
125
+ toolResults: [],
126
+ iterationCount: 0
127
+ };
128
+ }
129
+
130
+ let currentResponse = initialResponse;
131
+ let toolMessages = [];
132
+ let toolResults = [];
133
+ let toolCallCount = 0;
134
+ let iterationCount = 0;
135
+ const maxToolCalls = options.maxToolCalls || 20;
136
+ const maxIterations = options.maxIterations || Number.POSITIVE_INFINITY;
137
+
138
+ while (true) {
139
+ const toolCalls = parseToolUseBlocks(currentResponse);
140
+ if (toolCalls.length === 0) break;
141
+ if (iterationCount >= maxIterations) break;
142
+
143
+ toolCallCount += toolCalls.length;
144
+ if (toolCallCount > maxToolCalls) break;
145
+ iterationCount += 1;
146
+
147
+ const results = await executeToolCalls(toolCalls, registry, policy, agentConfig, {
148
+ approvalGate,
149
+ jobId: options.jobId,
150
+ agentId: options.agentId,
151
+ skipApprovalNotification: options.skipApprovalNotification === true,
152
+ });
153
+ toolResults = [...toolResults, ...results];
154
+
155
+ const pendingApprovals = results.filter((entry) => entry.pendingApproval);
156
+ if (pendingApprovals.length > 0) {
157
+ return {
158
+ finalResponse: currentResponse,
159
+ toolResults,
160
+ iterationCount,
161
+ pendingApprovalResult: {
162
+ summary: "Tool execution paused pending approval.",
163
+ output: buildPendingApprovalOutput(pendingApprovals),
164
+ nextActions: [
165
+ "Approve or deny the pending exec action from Telegram, then retry the request if needed."
166
+ ],
167
+ toolCalls: toolCalls.map((call, index) => ({
168
+ ...call,
169
+ output: results[index]?.output,
170
+ pendingApproval: results[index]?.pendingApproval || false,
171
+ approvalId: results[index]?.approvalId || null,
172
+ })),
173
+ pendingApproval: true,
174
+ raw: currentResponse,
175
+ }
176
+ };
177
+ }
178
+
179
+ const formattedResults = formatToolResults(results);
180
+ const continuationMessages = [
181
+ ...invocation.messages,
182
+ ...toolMessages,
183
+ { role: "assistant", content: currentResponse.content },
184
+ ...formattedResults,
185
+ ];
186
+ toolMessages = [
187
+ ...toolMessages,
188
+ { role: "assistant", content: currentResponse.content },
189
+ ...formattedResults,
190
+ ];
191
+
192
+ currentResponse = await adapter.invoke({ ...invocation, messages: continuationMessages });
193
+ }
194
+
195
+ return {
196
+ finalResponse: currentResponse,
197
+ toolResults,
198
+ iterationCount
199
+ };
200
+ }
@@ -0,0 +1,311 @@
1
+ import http from "node:http";
2
+ import path from "node:path";
3
+ import { ensureDir, listFilesRecursive, readJson, writeJson } from "../utils/fs.js";
4
+ import { buildRetentionPolicy, pruneJsonBuckets } from "./retention.js";
5
+ import { TelegramInboundHandler } from "./telegram-inbound.js";
6
+ import { SlackInboundHandler } from "./slack-inbound.js";
7
+
8
+ function timestampKey() {
9
+ return new Date().toISOString().replace(/[:.]/g, "-");
10
+ }
11
+
12
+ function sendJson(response, statusCode, body) {
13
+ response.writeHead(statusCode, {
14
+ "content-type": "application/json"
15
+ });
16
+ response.end(`${JSON.stringify(body, null, 2)}\n`);
17
+ }
18
+
19
+ async function readJsonBody(request, { maxBodyBytes = 1024 * 1024, timeoutMs = 5000 } = {}) {
20
+ const chunks = [];
21
+ let total = 0;
22
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Request body timed out")), timeoutMs));
23
+ const reader = (async () => {
24
+ for await (const chunk of request) {
25
+ total += chunk.length;
26
+ if (total > maxBodyBytes) {
27
+ throw new Error("Request body too large");
28
+ }
29
+ chunks.push(chunk);
30
+ }
31
+ const raw = Buffer.concat(chunks).toString("utf8");
32
+ return raw ? JSON.parse(raw) : {};
33
+ })();
34
+ return Promise.race([reader, timeout]);
35
+ }
36
+
37
+ async function readRawBody(request, { maxBodyBytes = 1024 * 1024, timeoutMs = 5000 } = {}) {
38
+ const chunks = [];
39
+ let total = 0;
40
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Request body timed out")), timeoutMs));
41
+ const reader = (async () => {
42
+ for await (const chunk of request) {
43
+ total += chunk.length;
44
+ if (total > maxBodyBytes) {
45
+ throw new Error("Request body too large");
46
+ }
47
+ chunks.push(chunk);
48
+ }
49
+ return Buffer.concat(chunks).toString("utf8");
50
+ })();
51
+ return Promise.race([reader, timeout]);
52
+ }
53
+
54
+ export class StandaloneTransportServer {
55
+ constructor({ stateRoot, authToken = null, retention = {}, maxBodyBytes = 1024 * 1024, requestTimeoutMs = 5000, allowedTargetModes = null, rateLimit = {} } = {}) {
56
+ this.stateRoot = stateRoot;
57
+ this.authToken = authToken;
58
+ this.server = null;
59
+ this.shuttingDown = false;
60
+ this.retentionPolicy = buildRetentionPolicy(retention, {
61
+ ttlDays: 7,
62
+ maxFilesPerBucket: 1000
63
+ });
64
+ this.maxBodyBytes = maxBodyBytes;
65
+ this.requestTimeoutMs = requestTimeoutMs;
66
+ this.allowedTargetModes = new Set(allowedTargetModes || ["operator", "same_thread", "peer_agent", "scheduler_log"]);
67
+ this.rateLimit = {
68
+ windowMs: Number(rateLimit.windowMs ?? 60_000),
69
+ maxRequests: Number(rateLimit.maxRequests ?? 120)
70
+ };
71
+ this.rateBuckets = new Map();
72
+ }
73
+
74
+ get transportRoot() {
75
+ return path.join(this.stateRoot, "transport");
76
+ }
77
+
78
+ attachTelegramInbound({
79
+ stateStore,
80
+ telegramConfig,
81
+ availablePeers = [],
82
+ contextLedger = null,
83
+ agentNames = {},
84
+ routerConfig = null,
85
+ agentConfigs = null,
86
+ }) {
87
+ this.telegramHandler = new TelegramInboundHandler({
88
+ stateStore,
89
+ telegramConfig,
90
+ availablePeers,
91
+ contextLedger,
92
+ agentNames,
93
+ routerConfig,
94
+ agentConfigs,
95
+ });
96
+ }
97
+
98
+ attachSlackInbound({ stateStore, slackConfig, availablePeers = [] }) {
99
+ this.slackHandler = new SlackInboundHandler({ stateStore, slackConfig, availablePeers });
100
+ }
101
+
102
+ async handle(request, response) {
103
+ try {
104
+ if (this.shuttingDown) {
105
+ sendJson(response, 503, {
106
+ ok: false,
107
+ error: "Server shutting down"
108
+ });
109
+ return;
110
+ }
111
+
112
+ if (request.method === "GET" && request.url === "/health") {
113
+ const recentCount = (await listFilesRecursive(path.join(this.transportRoot, "inbox"))).length;
114
+ sendJson(response, 200, {
115
+ ok: true,
116
+ transport: "standalone_http",
117
+ recentMessages: recentCount
118
+ });
119
+ return;
120
+ }
121
+
122
+ // Telegram webhook — must return 200 synchronously before any processing
123
+ if (request.method === "POST" && request.url === "/telegram/webhook") {
124
+ if (!this.telegramHandler) {
125
+ sendJson(response, 404, { ok: false, error: "Telegram inbound not configured" });
126
+ return;
127
+ }
128
+ // Validate webhook secret if configured
129
+ const webhookSecret = process.env.NEMORIS_TELEGRAM_WEBHOOK_SECRET;
130
+ if (webhookSecret) {
131
+ const provided = request.headers["x-telegram-bot-api-secret-token"] || "";
132
+ if (provided !== webhookSecret) {
133
+ sendJson(response, 401, { ok: false, error: "Invalid webhook secret" });
134
+ return;
135
+ }
136
+ }
137
+ const update = await readJsonBody(request, {
138
+ maxBodyBytes: this.maxBodyBytes,
139
+ timeoutMs: this.requestTimeoutMs,
140
+ });
141
+ const result = await this.telegramHandler.handleUpdate(update);
142
+ // Always return 200 to Telegram — regardless of outcome
143
+ sendJson(response, 200, { ok: true, ...result });
144
+ return;
145
+ }
146
+
147
+ // Slack Events API
148
+ if (request.method === "POST" && request.url === "/slack/events") {
149
+ if (!this.slackHandler) {
150
+ sendJson(response, 404, { ok: false, error: "Slack inbound not configured" });
151
+ return;
152
+ }
153
+ const rawBody = await readRawBody(request, {
154
+ maxBodyBytes: this.maxBodyBytes,
155
+ timeoutMs: this.requestTimeoutMs,
156
+ });
157
+
158
+ if (!this.slackHandler.verifySignature(request.headers, rawBody)) {
159
+ sendJson(response, 401, { ok: false, error: "Invalid signature" });
160
+ return;
161
+ }
162
+
163
+ const payload = JSON.parse(rawBody);
164
+ const result = this.slackHandler.handleEvent(payload);
165
+
166
+ // Handle URL verification challenge
167
+ if (result.action === "challenge") {
168
+ sendJson(response, 200, { challenge: result.challenge });
169
+ return;
170
+ }
171
+
172
+ // Slack requires immediate 200 OK
173
+ sendJson(response, 200, { ok: true });
174
+ return;
175
+ }
176
+
177
+ // Slack Slash Commands
178
+ if (request.method === "POST" && request.url === "/slack/slash") {
179
+ if (!this.slackHandler) {
180
+ sendJson(response, 404, { ok: false, error: "Slack inbound not configured" });
181
+ return;
182
+ }
183
+ const rawBody = await readRawBody(request, {
184
+ maxBodyBytes: this.maxBodyBytes,
185
+ timeoutMs: this.requestTimeoutMs,
186
+ });
187
+
188
+ if (!this.slackHandler.verifySignature(request.headers, rawBody)) {
189
+ sendJson(response, 401, { ok: false, error: "Invalid signature" });
190
+ return;
191
+ }
192
+
193
+ const params = new URLSearchParams(rawBody);
194
+ const payload = Object.fromEntries(params.entries());
195
+ const reply = this.slackHandler.handleSlashCommand(payload);
196
+
197
+ // Slack renders 200 response body as an inline message
198
+ response.writeHead(200, { "Content-Type": "text/plain" });
199
+ response.end(reply);
200
+ return;
201
+ }
202
+
203
+ if (request.method === "POST" && request.url === "/messages") {
204
+ if (this.authToken) {
205
+ const header = request.headers.authorization || "";
206
+ if (header !== `Bearer ${this.authToken}`) {
207
+ sendJson(response, 401, {
208
+ ok: false,
209
+ error: "Unauthorized"
210
+ });
211
+ return;
212
+ }
213
+ }
214
+
215
+ const rateKey = request.socket.remoteAddress || request.headers.authorization || "anonymous";
216
+ if (!this.allowRequest(rateKey)) {
217
+ sendJson(response, 429, {
218
+ ok: false,
219
+ error: "Rate limit exceeded"
220
+ });
221
+ return;
222
+ }
223
+
224
+ const payload = await readJsonBody(request, {
225
+ maxBodyBytes: this.maxBodyBytes,
226
+ timeoutMs: this.requestTimeoutMs
227
+ });
228
+ const bucket = String(payload.target?.mode || "operator");
229
+ if (!this.allowedTargetModes.has(bucket)) {
230
+ sendJson(response, 400, {
231
+ ok: false,
232
+ error: "Invalid target mode"
233
+ });
234
+ return;
235
+ }
236
+ const filePath = path.join(this.transportRoot, "inbox", bucket, `${timestampKey()}.json`);
237
+ await writeJson(filePath, {
238
+ receivedAt: new Date().toISOString(),
239
+ ...payload
240
+ });
241
+ await pruneJsonBuckets(path.join(this.transportRoot, "inbox"), this.retentionPolicy);
242
+ sendJson(response, 202, {
243
+ ok: true,
244
+ stored: filePath,
245
+ bucket
246
+ });
247
+ return;
248
+ }
249
+
250
+ sendJson(response, 404, {
251
+ ok: false,
252
+ error: "Not found"
253
+ });
254
+ } catch (error) {
255
+ if (String(error.message || "").includes("too large")) {
256
+ sendJson(response, 413, {
257
+ ok: false,
258
+ error: error.message
259
+ });
260
+ return;
261
+ }
262
+ sendJson(response, 500, {
263
+ ok: false,
264
+ error: error.message
265
+ });
266
+ }
267
+ }
268
+
269
+ async listen(port = 4318, host = "127.0.0.1") {
270
+ await ensureDir(this.transportRoot);
271
+ this.server = http.createServer((request, response) => {
272
+ this.handle(request, response);
273
+ });
274
+
275
+ return new Promise((resolve, reject) => {
276
+ this.server.once("error", reject);
277
+ this.server.listen(port, host, () => {
278
+ const address = this.server.address();
279
+ resolve(address);
280
+ });
281
+ });
282
+ }
283
+
284
+ async close() {
285
+ if (!this.server) return;
286
+ this.shuttingDown = true;
287
+ await new Promise((resolve, reject) => {
288
+ this.server.close((error) => (error ? reject(error) : resolve()));
289
+ });
290
+ this.server = null;
291
+ }
292
+
293
+ async listInbox(limit = 20) {
294
+ const files = await listFilesRecursive(path.join(this.transportRoot, "inbox"));
295
+ const sorted = [...files].sort().reverse().slice(0, limit);
296
+ return Promise.all(sorted.map((filePath) => readJson(filePath, null)));
297
+ }
298
+
299
+ allowRequest(key) {
300
+ const now = Date.now();
301
+ const windowStart = now - this.rateLimit.windowMs;
302
+ const current = (this.rateBuckets.get(key) || []).filter((timestamp) => timestamp >= windowStart);
303
+ if (current.length >= this.rateLimit.maxRequests) {
304
+ this.rateBuckets.set(key, current);
305
+ return false;
306
+ }
307
+ current.push(now);
308
+ this.rateBuckets.set(key, current);
309
+ return true;
310
+ }
311
+ }