nemoris 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,216 +1,216 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ExecApprovalGate — pauses dangerous tool calls and requires
|
|
5
|
-
* Telegram approval before they proceed.
|
|
6
|
-
*
|
|
7
|
-
* Opt-in per agent via `exec_approvals = true` in agent config.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const DANGEROUS_TOOLS = new Set(["shell_exec", "write_file"]);
|
|
11
|
-
const DANGEROUS_HTTP_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
12
|
-
const LOG_CAP = 1000;
|
|
13
|
-
|
|
14
|
-
function getExecApprovalSettings(agentConfig) {
|
|
15
|
-
const config = agentConfig?.execApprovals ?? agentConfig?.exec_approvals ?? false;
|
|
16
|
-
if (!config) {
|
|
17
|
-
return { enabled: false, allowTools: new Set() };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (config === true) {
|
|
21
|
-
return { enabled: true, allowTools: new Set() };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const allowTools = config.allowTools
|
|
25
|
-
|| config.allow_tools
|
|
26
|
-
|| config.allowedTools
|
|
27
|
-
|| config.allowed_tools
|
|
28
|
-
|| [];
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
enabled: config.enabled !== false,
|
|
32
|
-
allowTools: new Set((Array.isArray(allowTools) ? allowTools : []).map((toolName) => String(toolName)))
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class ExecApprovalGate {
|
|
37
|
-
/**
|
|
38
|
-
* @param {object} opts
|
|
39
|
-
* @param {function} opts.sendFn — async (text) => void, sends a Telegram message
|
|
40
|
-
* @param {number} [opts.timeoutMs=300000] — auto-deny timeout (5 min default)
|
|
41
|
-
*/
|
|
42
|
-
constructor({ sendFn, timeoutMs = 300_000 }) {
|
|
43
|
-
this._sendFn = sendFn;
|
|
44
|
-
this._timeoutMs = timeoutMs;
|
|
45
|
-
/** @type {Map<string, { resolve: Function, timer: ReturnType<typeof setTimeout>, toolName: string, toolInput: any, jobId: string, agentId: string, createdAt: string }>} */
|
|
46
|
-
this._pending = new Map();
|
|
47
|
-
/** @type {Array<object>} */
|
|
48
|
-
this._log = [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Check whether a tool call requires human approval.
|
|
53
|
-
* @param {string} toolName
|
|
54
|
-
* @param {object} toolInput
|
|
55
|
-
* @param {object} agentConfig — parsed agent TOML
|
|
56
|
-
* @returns {boolean}
|
|
57
|
-
*/
|
|
58
|
-
requiresApproval(toolName, toolInput, agentConfig) {
|
|
59
|
-
const settings = getExecApprovalSettings(agentConfig);
|
|
60
|
-
if (!settings.enabled) return false;
|
|
61
|
-
if (settings.allowTools.has(toolName)) return false;
|
|
62
|
-
|
|
63
|
-
if (DANGEROUS_TOOLS.has(toolName)) return true;
|
|
64
|
-
|
|
65
|
-
if (toolName === "http_fetch") {
|
|
66
|
-
const method = (toolInput?.method || "GET").toUpperCase();
|
|
67
|
-
return DANGEROUS_HTTP_METHODS.has(method);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (toolName.startsWith("mcp:")) return true;
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Request human approval for a tool call.
|
|
77
|
-
* Sends a Telegram message and returns a promise that resolves
|
|
78
|
-
* when /exec_approve or /exec_deny is called (or times out).
|
|
79
|
-
*
|
|
80
|
-
* @param {object} opts
|
|
81
|
-
* @param {string} opts.toolName
|
|
82
|
-
* @param {object} opts.toolInput
|
|
83
|
-
* @param {string} opts.jobId
|
|
84
|
-
* @param {string} opts.agentId
|
|
85
|
-
* @returns {Promise<{ approved: boolean, approvedBy?: string }>}
|
|
86
|
-
*/
|
|
87
|
-
requestApproval({ toolName, toolInput, jobId, agentId, skipNotify = false }) {
|
|
88
|
-
const approvalId = crypto.randomUUID();
|
|
89
|
-
|
|
90
|
-
const approvalPromise = new Promise((resolve) => {
|
|
91
|
-
const timer = setTimeout(() => {
|
|
92
|
-
if (this._pending.has(approvalId)) {
|
|
93
|
-
this._pending.delete(approvalId);
|
|
94
|
-
this._logDecision({
|
|
95
|
-
approvalId,
|
|
96
|
-
toolName,
|
|
97
|
-
action: "auto_deny",
|
|
98
|
-
approved: false,
|
|
99
|
-
approvedBy: "timeout",
|
|
100
|
-
timestamp: new Date().toISOString(),
|
|
101
|
-
});
|
|
102
|
-
if (!skipNotify) {
|
|
103
|
-
this._sendFn(`Approval ${approvalId} auto-denied (timed out after ${this._timeoutMs / 1000}s).`).catch(() => {});
|
|
104
|
-
}
|
|
105
|
-
resolve({ approved: false });
|
|
106
|
-
}
|
|
107
|
-
}, this._timeoutMs);
|
|
108
|
-
timer.unref?.();
|
|
109
|
-
|
|
110
|
-
this._pending.set(approvalId, {
|
|
111
|
-
resolve,
|
|
112
|
-
timer,
|
|
113
|
-
toolName,
|
|
114
|
-
toolInput,
|
|
115
|
-
jobId,
|
|
116
|
-
agentId,
|
|
117
|
-
createdAt: new Date().toISOString(),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Build detail string for the message
|
|
121
|
-
let detail = "";
|
|
122
|
-
if (toolName === "shell_exec") {
|
|
123
|
-
detail = `Command: ${toolInput?.command || JSON.stringify(toolInput)}`;
|
|
124
|
-
} else if (toolName === "write_file") {
|
|
125
|
-
detail = `Path: ${toolInput?.path || toolInput?.file_path || JSON.stringify(toolInput)}`;
|
|
126
|
-
} else if (toolName === "http_fetch") {
|
|
127
|
-
detail = `${toolInput?.method || "?"} ${toolInput?.url || "?"}`;
|
|
128
|
-
} else {
|
|
129
|
-
detail = JSON.stringify(toolInput || {}).slice(0, 200);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const msg = [
|
|
133
|
-
`\u{1F512} Approval needed`,
|
|
134
|
-
``,
|
|
135
|
-
`Tool: ${toolName}`,
|
|
136
|
-
`${detail}`,
|
|
137
|
-
`Requester: interactive turn (job #${jobId})`,
|
|
138
|
-
``,
|
|
139
|
-
`Reply /exec_approve ${approvalId} or /exec_deny ${approvalId}`,
|
|
140
|
-
`Auto-denied in ${Math.floor(this._timeoutMs / 60000)} minutes if no response.`,
|
|
141
|
-
].join("\n");
|
|
142
|
-
|
|
143
|
-
if (!skipNotify) {
|
|
144
|
-
this._sendFn(msg).catch(() => {});
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
approvalPromise.approvalId = approvalId;
|
|
149
|
-
return approvalPromise;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Resolve a pending approval.
|
|
154
|
-
* @param {string} approvalId
|
|
155
|
-
* @param {boolean} approved
|
|
156
|
-
* @param {string} [approvedBy]
|
|
157
|
-
* @returns {boolean} — true if the approval was found and resolved
|
|
158
|
-
*/
|
|
159
|
-
resolve(approvalId, approved, approvedBy) {
|
|
160
|
-
const entry = this._pending.get(approvalId);
|
|
161
|
-
if (!entry) return false;
|
|
162
|
-
|
|
163
|
-
clearTimeout(entry.timer);
|
|
164
|
-
this._pending.delete(approvalId);
|
|
165
|
-
|
|
166
|
-
this._logDecision({
|
|
167
|
-
approvalId,
|
|
168
|
-
toolName: entry.toolName,
|
|
169
|
-
action: approved ? "approved" : "denied",
|
|
170
|
-
approved,
|
|
171
|
-
approvedBy: approvedBy || "unknown",
|
|
172
|
-
timestamp: new Date().toISOString(),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
entry.resolve({ approved, approvedBy });
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get all pending approvals for status display.
|
|
181
|
-
* @returns {Array<{ approvalId: string, toolName: string, toolInput: any, jobId: string, agentId: string, createdAt: string }>}
|
|
182
|
-
*/
|
|
183
|
-
getPending() {
|
|
184
|
-
const results = [];
|
|
185
|
-
for (const [approvalId, entry] of this._pending) {
|
|
186
|
-
results.push({
|
|
187
|
-
approvalId,
|
|
188
|
-
toolName: entry.toolName,
|
|
189
|
-
toolInput: entry.toolInput,
|
|
190
|
-
jobId: entry.jobId,
|
|
191
|
-
agentId: entry.agentId,
|
|
192
|
-
createdAt: entry.createdAt,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
return results;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Append a decision to the in-memory audit log (capped at LOG_CAP).
|
|
200
|
-
* @param {object} decision
|
|
201
|
-
*/
|
|
202
|
-
_logDecision(decision) {
|
|
203
|
-
this._log.push(decision);
|
|
204
|
-
if (this._log.length > LOG_CAP) {
|
|
205
|
-
this._log = this._log.slice(this._log.length - LOG_CAP);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Return the full decision log (for debugging / admin).
|
|
211
|
-
* @returns {Array<object>}
|
|
212
|
-
*/
|
|
213
|
-
getLog() {
|
|
214
|
-
return this._log;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ExecApprovalGate — pauses dangerous tool calls and requires
|
|
5
|
+
* Telegram approval before they proceed.
|
|
6
|
+
*
|
|
7
|
+
* Opt-in per agent via `exec_approvals = true` in agent config.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DANGEROUS_TOOLS = new Set(["shell_exec", "write_file"]);
|
|
11
|
+
const DANGEROUS_HTTP_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
12
|
+
const LOG_CAP = 1000;
|
|
13
|
+
|
|
14
|
+
function getExecApprovalSettings(agentConfig) {
|
|
15
|
+
const config = agentConfig?.execApprovals ?? agentConfig?.exec_approvals ?? false;
|
|
16
|
+
if (!config) {
|
|
17
|
+
return { enabled: false, allowTools: new Set() };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (config === true) {
|
|
21
|
+
return { enabled: true, allowTools: new Set() };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const allowTools = config.allowTools
|
|
25
|
+
|| config.allow_tools
|
|
26
|
+
|| config.allowedTools
|
|
27
|
+
|| config.allowed_tools
|
|
28
|
+
|| [];
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
enabled: config.enabled !== false,
|
|
32
|
+
allowTools: new Set((Array.isArray(allowTools) ? allowTools : []).map((toolName) => String(toolName)))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ExecApprovalGate {
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {function} opts.sendFn — async (text) => void, sends a Telegram message
|
|
40
|
+
* @param {number} [opts.timeoutMs=300000] — auto-deny timeout (5 min default)
|
|
41
|
+
*/
|
|
42
|
+
constructor({ sendFn, timeoutMs = 300_000 }) {
|
|
43
|
+
this._sendFn = sendFn;
|
|
44
|
+
this._timeoutMs = timeoutMs;
|
|
45
|
+
/** @type {Map<string, { resolve: Function, timer: ReturnType<typeof setTimeout>, toolName: string, toolInput: any, jobId: string, agentId: string, createdAt: string }>} */
|
|
46
|
+
this._pending = new Map();
|
|
47
|
+
/** @type {Array<object>} */
|
|
48
|
+
this._log = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check whether a tool call requires human approval.
|
|
53
|
+
* @param {string} toolName
|
|
54
|
+
* @param {object} toolInput
|
|
55
|
+
* @param {object} agentConfig — parsed agent TOML
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
requiresApproval(toolName, toolInput, agentConfig) {
|
|
59
|
+
const settings = getExecApprovalSettings(agentConfig);
|
|
60
|
+
if (!settings.enabled) return false;
|
|
61
|
+
if (settings.allowTools.has(toolName)) return false;
|
|
62
|
+
|
|
63
|
+
if (DANGEROUS_TOOLS.has(toolName)) return true;
|
|
64
|
+
|
|
65
|
+
if (toolName === "http_fetch") {
|
|
66
|
+
const method = (toolInput?.method || "GET").toUpperCase();
|
|
67
|
+
return DANGEROUS_HTTP_METHODS.has(method);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (toolName.startsWith("mcp:")) return true;
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Request human approval for a tool call.
|
|
77
|
+
* Sends a Telegram message and returns a promise that resolves
|
|
78
|
+
* when /exec_approve or /exec_deny is called (or times out).
|
|
79
|
+
*
|
|
80
|
+
* @param {object} opts
|
|
81
|
+
* @param {string} opts.toolName
|
|
82
|
+
* @param {object} opts.toolInput
|
|
83
|
+
* @param {string} opts.jobId
|
|
84
|
+
* @param {string} opts.agentId
|
|
85
|
+
* @returns {Promise<{ approved: boolean, approvedBy?: string }>}
|
|
86
|
+
*/
|
|
87
|
+
requestApproval({ toolName, toolInput, jobId, agentId, skipNotify = false }) {
|
|
88
|
+
const approvalId = crypto.randomUUID();
|
|
89
|
+
|
|
90
|
+
const approvalPromise = new Promise((resolve) => {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
if (this._pending.has(approvalId)) {
|
|
93
|
+
this._pending.delete(approvalId);
|
|
94
|
+
this._logDecision({
|
|
95
|
+
approvalId,
|
|
96
|
+
toolName,
|
|
97
|
+
action: "auto_deny",
|
|
98
|
+
approved: false,
|
|
99
|
+
approvedBy: "timeout",
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
});
|
|
102
|
+
if (!skipNotify) {
|
|
103
|
+
this._sendFn(`Approval ${approvalId} auto-denied (timed out after ${this._timeoutMs / 1000}s).`).catch(() => {});
|
|
104
|
+
}
|
|
105
|
+
resolve({ approved: false });
|
|
106
|
+
}
|
|
107
|
+
}, this._timeoutMs);
|
|
108
|
+
timer.unref?.();
|
|
109
|
+
|
|
110
|
+
this._pending.set(approvalId, {
|
|
111
|
+
resolve,
|
|
112
|
+
timer,
|
|
113
|
+
toolName,
|
|
114
|
+
toolInput,
|
|
115
|
+
jobId,
|
|
116
|
+
agentId,
|
|
117
|
+
createdAt: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Build detail string for the message
|
|
121
|
+
let detail = "";
|
|
122
|
+
if (toolName === "shell_exec") {
|
|
123
|
+
detail = `Command: ${toolInput?.command || JSON.stringify(toolInput)}`;
|
|
124
|
+
} else if (toolName === "write_file") {
|
|
125
|
+
detail = `Path: ${toolInput?.path || toolInput?.file_path || JSON.stringify(toolInput)}`;
|
|
126
|
+
} else if (toolName === "http_fetch") {
|
|
127
|
+
detail = `${toolInput?.method || "?"} ${toolInput?.url || "?"}`;
|
|
128
|
+
} else {
|
|
129
|
+
detail = JSON.stringify(toolInput || {}).slice(0, 200);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const msg = [
|
|
133
|
+
`\u{1F512} Approval needed`,
|
|
134
|
+
``,
|
|
135
|
+
`Tool: ${toolName}`,
|
|
136
|
+
`${detail}`,
|
|
137
|
+
`Requester: interactive turn (job #${jobId})`,
|
|
138
|
+
``,
|
|
139
|
+
`Reply /exec_approve ${approvalId} or /exec_deny ${approvalId}`,
|
|
140
|
+
`Auto-denied in ${Math.floor(this._timeoutMs / 60000)} minutes if no response.`,
|
|
141
|
+
].join("\n");
|
|
142
|
+
|
|
143
|
+
if (!skipNotify) {
|
|
144
|
+
this._sendFn(msg).catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
approvalPromise.approvalId = approvalId;
|
|
149
|
+
return approvalPromise;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a pending approval.
|
|
154
|
+
* @param {string} approvalId
|
|
155
|
+
* @param {boolean} approved
|
|
156
|
+
* @param {string} [approvedBy]
|
|
157
|
+
* @returns {boolean} — true if the approval was found and resolved
|
|
158
|
+
*/
|
|
159
|
+
resolve(approvalId, approved, approvedBy) {
|
|
160
|
+
const entry = this._pending.get(approvalId);
|
|
161
|
+
if (!entry) return false;
|
|
162
|
+
|
|
163
|
+
clearTimeout(entry.timer);
|
|
164
|
+
this._pending.delete(approvalId);
|
|
165
|
+
|
|
166
|
+
this._logDecision({
|
|
167
|
+
approvalId,
|
|
168
|
+
toolName: entry.toolName,
|
|
169
|
+
action: approved ? "approved" : "denied",
|
|
170
|
+
approved,
|
|
171
|
+
approvedBy: approvedBy || "unknown",
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
entry.resolve({ approved, approvedBy });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all pending approvals for status display.
|
|
181
|
+
* @returns {Array<{ approvalId: string, toolName: string, toolInput: any, jobId: string, agentId: string, createdAt: string }>}
|
|
182
|
+
*/
|
|
183
|
+
getPending() {
|
|
184
|
+
const results = [];
|
|
185
|
+
for (const [approvalId, entry] of this._pending) {
|
|
186
|
+
results.push({
|
|
187
|
+
approvalId,
|
|
188
|
+
toolName: entry.toolName,
|
|
189
|
+
toolInput: entry.toolInput,
|
|
190
|
+
jobId: entry.jobId,
|
|
191
|
+
agentId: entry.agentId,
|
|
192
|
+
createdAt: entry.createdAt,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Append a decision to the in-memory audit log (capped at LOG_CAP).
|
|
200
|
+
* @param {object} decision
|
|
201
|
+
*/
|
|
202
|
+
_logDecision(decision) {
|
|
203
|
+
this._log.push(decision);
|
|
204
|
+
if (this._log.length > LOG_CAP) {
|
|
205
|
+
this._log = this._log.slice(this._log.length - LOG_CAP);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Return the full decision log (for debugging / admin).
|
|
211
|
+
* @returns {Array<object>}
|
|
212
|
+
*/
|
|
213
|
+
getLog() {
|
|
214
|
+
return this._log;
|
|
215
|
+
}
|
|
216
|
+
}
|