@elizaos/plugin-shell 2.0.3-beta.2 → 2.0.3-beta.4
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/dist/approvals/allowlist.d.ts +76 -0
- package/dist/approvals/allowlist.d.ts.map +1 -0
- package/dist/approvals/analysis.d.ts +76 -0
- package/dist/approvals/analysis.d.ts.map +1 -0
- package/dist/approvals/index.d.ts +12 -0
- package/dist/approvals/index.d.ts.map +1 -0
- package/dist/approvals/service.d.ts +121 -0
- package/dist/approvals/service.d.ts.map +1 -0
- package/dist/approvals/types.d.ts +219 -0
- package/dist/approvals/types.d.ts.map +1 -0
- package/dist/auto-enable.d.ts +4 -0
- package/dist/auto-enable.d.ts.map +1 -0
- package/dist/generated/specs/spec-helpers.d.ts +36 -0
- package/dist/generated/specs/spec-helpers.d.ts.map +1 -0
- package/dist/generated/specs/specs.d.ts +48 -0
- package/dist/generated/specs/specs.d.ts.map +1 -0
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4284 -0
- package/dist/index.js.map +24 -0
- package/dist/prompts.d.ts +11 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/shellHistoryProvider.d.ts +4 -0
- package/dist/providers/shellHistoryProvider.d.ts.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/processRegistry.d.ts +25 -0
- package/dist/services/processRegistry.d.ts.map +1 -0
- package/dist/services/shellService.d.ts +95 -0
- package/dist/services/shellService.d.ts.map +1 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/pathUtils.d.ts +5 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/dist/utils/processQueue.d.ts +136 -0
- package/dist/utils/processQueue.d.ts.map +1 -0
- package/dist/utils/ptyKeys.d.ts +23 -0
- package/dist/utils/ptyKeys.d.ts.map +1 -0
- package/dist/utils/shellArgv.d.ts +37 -0
- package/dist/utils/shellArgv.d.ts.map +1 -0
- package/dist/utils/shellUtils.d.ts +103 -0
- package/dist/utils/shellUtils.d.ts.map +1 -0
- package/dist/utils/terminalCapabilities.d.ts +30 -0
- package/dist/utils/terminalCapabilities.d.ts.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/package.json +5 -4
- package/registry-entry.json +94 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4284 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// approvals/allowlist.ts
|
|
5
|
+
import crypto2 from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { logger, resolveStateDir } from "@elizaos/core";
|
|
10
|
+
|
|
11
|
+
// approvals/types.ts
|
|
12
|
+
var DEFAULT_SAFE_BINS = [
|
|
13
|
+
"jq",
|
|
14
|
+
"grep",
|
|
15
|
+
"cut",
|
|
16
|
+
"sort",
|
|
17
|
+
"uniq",
|
|
18
|
+
"head",
|
|
19
|
+
"tail",
|
|
20
|
+
"tr",
|
|
21
|
+
"wc"
|
|
22
|
+
];
|
|
23
|
+
var EXEC_APPROVAL_DEFAULTS = {
|
|
24
|
+
security: "deny",
|
|
25
|
+
ask: "on-miss",
|
|
26
|
+
askFallback: "deny",
|
|
27
|
+
autoAllowSkills: false,
|
|
28
|
+
timeoutMs: 120000
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// approvals/allowlist.ts
|
|
32
|
+
var DEFAULT_AGENT_ID = "default";
|
|
33
|
+
function expandHome(value) {
|
|
34
|
+
if (!value)
|
|
35
|
+
return value;
|
|
36
|
+
if (value === "~")
|
|
37
|
+
return os.homedir();
|
|
38
|
+
if (value.startsWith("~/"))
|
|
39
|
+
return path.join(os.homedir(), value.slice(2));
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function hashContent(raw) {
|
|
43
|
+
return crypto2.createHash("sha256").update(raw ?? "").digest("hex");
|
|
44
|
+
}
|
|
45
|
+
function getApprovalFilePath() {
|
|
46
|
+
return path.join(resolveStateDir(), "exec-approvals.json");
|
|
47
|
+
}
|
|
48
|
+
function getApprovalSocketPath() {
|
|
49
|
+
return path.join(resolveStateDir(), "exec-approvals.sock");
|
|
50
|
+
}
|
|
51
|
+
function ensureDir(filePath) {
|
|
52
|
+
const dir = path.dirname(filePath);
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
function normalizePattern(value) {
|
|
56
|
+
const trimmed = value?.trim() ?? "";
|
|
57
|
+
return trimmed ? trimmed.toLowerCase() : null;
|
|
58
|
+
}
|
|
59
|
+
function ensureAllowlistIds(allowlist) {
|
|
60
|
+
if (!Array.isArray(allowlist) || allowlist.length === 0) {
|
|
61
|
+
return allowlist;
|
|
62
|
+
}
|
|
63
|
+
let changed = false;
|
|
64
|
+
const next = allowlist.map((entry) => {
|
|
65
|
+
if (entry.id)
|
|
66
|
+
return entry;
|
|
67
|
+
changed = true;
|
|
68
|
+
return { ...entry, id: crypto2.randomUUID() };
|
|
69
|
+
});
|
|
70
|
+
return changed ? next : allowlist;
|
|
71
|
+
}
|
|
72
|
+
function mergeLegacyAgent(current, legacy) {
|
|
73
|
+
const allowlist = [];
|
|
74
|
+
const seen = new Set;
|
|
75
|
+
const pushEntry = (entry) => {
|
|
76
|
+
const key = normalizePattern(entry.pattern);
|
|
77
|
+
if (!key || seen.has(key))
|
|
78
|
+
return;
|
|
79
|
+
seen.add(key);
|
|
80
|
+
allowlist.push(entry);
|
|
81
|
+
};
|
|
82
|
+
for (const entry of current.allowlist ?? [])
|
|
83
|
+
pushEntry(entry);
|
|
84
|
+
for (const entry of legacy.allowlist ?? [])
|
|
85
|
+
pushEntry(entry);
|
|
86
|
+
return {
|
|
87
|
+
security: current.security ?? legacy.security,
|
|
88
|
+
ask: current.ask ?? legacy.ask,
|
|
89
|
+
askFallback: current.askFallback ?? legacy.askFallback,
|
|
90
|
+
autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills,
|
|
91
|
+
allowlist: allowlist.length > 0 ? allowlist : undefined
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizeApprovals(file) {
|
|
95
|
+
const socketPath = file.socket?.path?.trim();
|
|
96
|
+
const token = file.socket?.token?.trim();
|
|
97
|
+
const agents = { ...file.agents };
|
|
98
|
+
const legacyDefault = agents.default;
|
|
99
|
+
if (legacyDefault) {
|
|
100
|
+
const main = agents[DEFAULT_AGENT_ID];
|
|
101
|
+
agents[DEFAULT_AGENT_ID] = main ? mergeLegacyAgent(main, legacyDefault) : legacyDefault;
|
|
102
|
+
delete agents.default;
|
|
103
|
+
}
|
|
104
|
+
for (const [key, agent] of Object.entries(agents)) {
|
|
105
|
+
const allowlist = ensureAllowlistIds(agent.allowlist);
|
|
106
|
+
if (allowlist !== agent.allowlist) {
|
|
107
|
+
agents[key] = { ...agent, allowlist };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
version: 1,
|
|
112
|
+
socket: {
|
|
113
|
+
path: socketPath && socketPath.length > 0 ? socketPath : undefined,
|
|
114
|
+
token: token && token.length > 0 ? token : undefined
|
|
115
|
+
},
|
|
116
|
+
defaults: {
|
|
117
|
+
security: file.defaults?.security,
|
|
118
|
+
ask: file.defaults?.ask,
|
|
119
|
+
askFallback: file.defaults?.askFallback,
|
|
120
|
+
autoAllowSkills: file.defaults?.autoAllowSkills
|
|
121
|
+
},
|
|
122
|
+
agents
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function generateToken() {
|
|
126
|
+
return crypto2.randomBytes(24).toString("base64url");
|
|
127
|
+
}
|
|
128
|
+
function readApprovalsSnapshot() {
|
|
129
|
+
const filePath = getApprovalFilePath();
|
|
130
|
+
if (!fs.existsSync(filePath)) {
|
|
131
|
+
const file2 = normalizeApprovals({ version: 1, agents: {} });
|
|
132
|
+
return {
|
|
133
|
+
path: filePath,
|
|
134
|
+
exists: false,
|
|
135
|
+
raw: null,
|
|
136
|
+
file: file2,
|
|
137
|
+
hash: hashContent(null)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
141
|
+
let parsed = null;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(raw);
|
|
144
|
+
} catch (parseError) {
|
|
145
|
+
logger.warn({ src: "exec-approval", parseError, filePath }, "Failed to parse approval config snapshot - file may be corrupted");
|
|
146
|
+
parsed = null;
|
|
147
|
+
}
|
|
148
|
+
const file = parsed?.version === 1 ? normalizeApprovals(parsed) : normalizeApprovals({ version: 1, agents: {} });
|
|
149
|
+
if (parsed && parsed.version !== 1) {
|
|
150
|
+
logger.warn({ src: "exec-approval", version: parsed.version, filePath }, "Approval config snapshot has unexpected version");
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
path: filePath,
|
|
154
|
+
exists: true,
|
|
155
|
+
raw,
|
|
156
|
+
file,
|
|
157
|
+
hash: hashContent(raw)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function loadApprovals() {
|
|
161
|
+
const filePath = getApprovalFilePath();
|
|
162
|
+
try {
|
|
163
|
+
if (!fs.existsSync(filePath)) {
|
|
164
|
+
logger.debug({ src: "exec-approval", filePath }, "Approval config file does not exist, using defaults");
|
|
165
|
+
return normalizeApprovals({ version: 1, agents: {} });
|
|
166
|
+
}
|
|
167
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(raw);
|
|
171
|
+
} catch (parseError) {
|
|
172
|
+
logger.error({ src: "exec-approval", parseError, filePath }, "Failed to parse approval config JSON - file may be corrupted. Using defaults.");
|
|
173
|
+
return normalizeApprovals({ version: 1, agents: {} });
|
|
174
|
+
}
|
|
175
|
+
if (parsed.version !== 1) {
|
|
176
|
+
logger.warn({ src: "exec-approval", version: parsed.version, filePath }, "Approval config has unexpected version, using defaults");
|
|
177
|
+
return normalizeApprovals({ version: 1, agents: {} });
|
|
178
|
+
}
|
|
179
|
+
return normalizeApprovals(parsed);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.error({ src: "exec-approval", error, filePath }, "Failed to load approval config - using defaults. This may indicate a permissions issue.");
|
|
182
|
+
return normalizeApprovals({ version: 1, agents: {} });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function saveApprovals(file) {
|
|
186
|
+
const filePath = getApprovalFilePath();
|
|
187
|
+
try {
|
|
188
|
+
ensureDir(filePath);
|
|
189
|
+
fs.writeFileSync(filePath, `${JSON.stringify(file, null, 2)}
|
|
190
|
+
`, {
|
|
191
|
+
mode: 384
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
fs.chmodSync(filePath, 384);
|
|
195
|
+
} catch {}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.error({ src: "exec-approval", error, filePath }, "Failed to save approval configuration");
|
|
198
|
+
throw new Error(`Failed to save approval configuration to ${filePath}: ${error}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function ensureApprovals() {
|
|
202
|
+
const loaded = loadApprovals();
|
|
203
|
+
const next = normalizeApprovals(loaded);
|
|
204
|
+
const socketPath = next.socket?.path?.trim();
|
|
205
|
+
const token = next.socket?.token?.trim();
|
|
206
|
+
const updated = {
|
|
207
|
+
...next,
|
|
208
|
+
socket: {
|
|
209
|
+
path: socketPath && socketPath.length > 0 ? socketPath : getApprovalSocketPath(),
|
|
210
|
+
token: token && token.length > 0 ? token : generateToken()
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
saveApprovals(updated);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.warn({ src: "exec-approval", error }, "Failed to save approval config during ensureApprovals - " + "returning in-memory config. Changes will not persist.");
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
return updated;
|
|
220
|
+
}
|
|
221
|
+
function normalizeSecurity(value, fallback) {
|
|
222
|
+
if (value === "allowlist" || value === "full" || value === "deny") {
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
return fallback;
|
|
226
|
+
}
|
|
227
|
+
function normalizeAsk(value, fallback) {
|
|
228
|
+
if (value === "always" || value === "off" || value === "on-miss") {
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
return fallback;
|
|
232
|
+
}
|
|
233
|
+
function resolveApprovals(agentId, overrides) {
|
|
234
|
+
let file;
|
|
235
|
+
try {
|
|
236
|
+
file = ensureApprovals();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
logger.warn({ src: "exec-approval", error }, "Could not ensure approval config exists - using read-only config");
|
|
239
|
+
file = loadApprovals();
|
|
240
|
+
}
|
|
241
|
+
return resolveApprovalsFromFile({
|
|
242
|
+
file,
|
|
243
|
+
agentId,
|
|
244
|
+
overrides,
|
|
245
|
+
path: getApprovalFilePath(),
|
|
246
|
+
socketPath: expandHome(file.socket?.path ?? getApprovalSocketPath()),
|
|
247
|
+
token: file.socket?.token ?? ""
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function resolveApprovalsFromFile(params) {
|
|
251
|
+
const file = normalizeApprovals(params.file);
|
|
252
|
+
const defaults = file.defaults ?? {};
|
|
253
|
+
const agentKey = params.agentId ?? DEFAULT_AGENT_ID;
|
|
254
|
+
const agent = file.agents?.[agentKey] ?? {};
|
|
255
|
+
const wildcard = file.agents?.["*"] ?? {};
|
|
256
|
+
const fallbackSecurity = params.overrides?.security ?? EXEC_APPROVAL_DEFAULTS.security;
|
|
257
|
+
const fallbackAsk = params.overrides?.ask ?? EXEC_APPROVAL_DEFAULTS.ask;
|
|
258
|
+
const fallbackAskFallback = params.overrides?.askFallback ?? EXEC_APPROVAL_DEFAULTS.askFallback;
|
|
259
|
+
const fallbackAutoAllowSkills = params.overrides?.autoAllowSkills ?? EXEC_APPROVAL_DEFAULTS.autoAllowSkills;
|
|
260
|
+
const resolvedDefaults = {
|
|
261
|
+
security: normalizeSecurity(defaults.security, fallbackSecurity),
|
|
262
|
+
ask: normalizeAsk(defaults.ask, fallbackAsk),
|
|
263
|
+
askFallback: normalizeSecurity(defaults.askFallback ?? fallbackAskFallback, fallbackAskFallback),
|
|
264
|
+
autoAllowSkills: Boolean(defaults.autoAllowSkills ?? fallbackAutoAllowSkills)
|
|
265
|
+
};
|
|
266
|
+
const resolvedAgent = {
|
|
267
|
+
security: normalizeSecurity(agent.security ?? wildcard.security ?? resolvedDefaults.security, resolvedDefaults.security),
|
|
268
|
+
ask: normalizeAsk(agent.ask ?? wildcard.ask ?? resolvedDefaults.ask, resolvedDefaults.ask),
|
|
269
|
+
askFallback: normalizeSecurity(agent.askFallback ?? wildcard.askFallback ?? resolvedDefaults.askFallback, resolvedDefaults.askFallback),
|
|
270
|
+
autoAllowSkills: Boolean(agent.autoAllowSkills ?? wildcard.autoAllowSkills ?? resolvedDefaults.autoAllowSkills)
|
|
271
|
+
};
|
|
272
|
+
const allowlist = [
|
|
273
|
+
...Array.isArray(wildcard.allowlist) ? wildcard.allowlist : [],
|
|
274
|
+
...Array.isArray(agent.allowlist) ? agent.allowlist : []
|
|
275
|
+
];
|
|
276
|
+
return {
|
|
277
|
+
path: params.path ?? getApprovalFilePath(),
|
|
278
|
+
socketPath: expandHome(params.socketPath ?? file.socket?.path ?? getApprovalSocketPath()),
|
|
279
|
+
token: params.token ?? file.socket?.token ?? "",
|
|
280
|
+
defaults: resolvedDefaults,
|
|
281
|
+
agent: resolvedAgent,
|
|
282
|
+
allowlist,
|
|
283
|
+
file
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function matchAllowlist(entries, resolution) {
|
|
287
|
+
if (!entries.length || !resolution?.resolvedPath) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const resolvedPath = resolution.resolvedPath;
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
const pattern = entry.pattern.trim();
|
|
293
|
+
if (!pattern)
|
|
294
|
+
continue;
|
|
295
|
+
const hasPath = pattern.includes("/") || pattern.includes("\\") || pattern.includes("~");
|
|
296
|
+
if (!hasPath)
|
|
297
|
+
continue;
|
|
298
|
+
if (matchesPattern(pattern, resolvedPath)) {
|
|
299
|
+
return entry;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
function matchesPattern(pattern, target) {
|
|
305
|
+
const trimmed = pattern.trim();
|
|
306
|
+
if (!trimmed)
|
|
307
|
+
return false;
|
|
308
|
+
const expanded = trimmed.startsWith("~") ? expandHome(trimmed) : trimmed;
|
|
309
|
+
const hasWildcard = /[*?]/.test(expanded);
|
|
310
|
+
let normalizedPattern = expanded;
|
|
311
|
+
let normalizedTarget = target;
|
|
312
|
+
if (process.platform === "win32" && !hasWildcard) {
|
|
313
|
+
normalizedPattern = tryRealpath(expanded) ?? expanded;
|
|
314
|
+
normalizedTarget = tryRealpath(target) ?? target;
|
|
315
|
+
}
|
|
316
|
+
normalizedPattern = normalizeMatchTarget(normalizedPattern);
|
|
317
|
+
normalizedTarget = normalizeMatchTarget(normalizedTarget);
|
|
318
|
+
const regex = globToRegExp(normalizedPattern);
|
|
319
|
+
return regex.test(normalizedTarget);
|
|
320
|
+
}
|
|
321
|
+
function normalizeMatchTarget(value) {
|
|
322
|
+
if (process.platform === "win32") {
|
|
323
|
+
const stripped = value.replace(/^\\\\[?.]\\/, "");
|
|
324
|
+
return stripped.replace(/\\/g, "/").toLowerCase();
|
|
325
|
+
}
|
|
326
|
+
return value.replace(/\\\\/g, "/").toLowerCase();
|
|
327
|
+
}
|
|
328
|
+
function tryRealpath(value) {
|
|
329
|
+
try {
|
|
330
|
+
return fs.realpathSync(value);
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function globToRegExp(pattern) {
|
|
336
|
+
let regex = "^";
|
|
337
|
+
let i = 0;
|
|
338
|
+
while (i < pattern.length) {
|
|
339
|
+
const ch = pattern[i];
|
|
340
|
+
if (ch === "*") {
|
|
341
|
+
const next = pattern[i + 1];
|
|
342
|
+
if (next === "*") {
|
|
343
|
+
regex += ".*";
|
|
344
|
+
i += 2;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
regex += "[^/]*";
|
|
348
|
+
i += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (ch === "?") {
|
|
352
|
+
regex += ".";
|
|
353
|
+
i += 1;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
regex += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
357
|
+
i += 1;
|
|
358
|
+
}
|
|
359
|
+
regex += "$";
|
|
360
|
+
return new RegExp(regex, "i");
|
|
361
|
+
}
|
|
362
|
+
function recordAllowlistUse(approvals, agentId, entry, command, resolvedPath) {
|
|
363
|
+
const target = agentId ?? DEFAULT_AGENT_ID;
|
|
364
|
+
const agents = approvals.agents ?? {};
|
|
365
|
+
const existing = agents[target] ?? {};
|
|
366
|
+
const allowlist = Array.isArray(existing.allowlist) ? existing.allowlist : [];
|
|
367
|
+
const nextAllowlist = allowlist.map((item) => item.pattern === entry.pattern ? {
|
|
368
|
+
...item,
|
|
369
|
+
id: item.id ?? crypto2.randomUUID(),
|
|
370
|
+
lastUsedAt: Date.now(),
|
|
371
|
+
lastUsedCommand: command,
|
|
372
|
+
lastResolvedPath: resolvedPath
|
|
373
|
+
} : item);
|
|
374
|
+
agents[target] = { ...existing, allowlist: nextAllowlist };
|
|
375
|
+
approvals.agents = agents;
|
|
376
|
+
try {
|
|
377
|
+
saveApprovals(approvals);
|
|
378
|
+
return true;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
logger.warn({ src: "exec-approval", error, pattern: entry.pattern }, "Failed to record allowlist usage - continuing without update");
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function addAllowlistEntry(approvals, agentId, pattern) {
|
|
385
|
+
const target = agentId ?? DEFAULT_AGENT_ID;
|
|
386
|
+
const agents = approvals.agents ?? {};
|
|
387
|
+
const existing = agents[target] ?? {};
|
|
388
|
+
const allowlist = Array.isArray(existing.allowlist) ? existing.allowlist : [];
|
|
389
|
+
const trimmed = pattern.trim();
|
|
390
|
+
if (!trimmed) {
|
|
391
|
+
logger.warn({ src: "exec-approval" }, "Attempted to add empty pattern to allowlist");
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (allowlist.some((entry) => entry.pattern === trimmed)) {
|
|
395
|
+
logger.debug({ src: "exec-approval", pattern: trimmed }, "Pattern already in allowlist");
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
allowlist.push({
|
|
399
|
+
id: crypto2.randomUUID(),
|
|
400
|
+
pattern: trimmed,
|
|
401
|
+
lastUsedAt: Date.now()
|
|
402
|
+
});
|
|
403
|
+
agents[target] = { ...existing, allowlist };
|
|
404
|
+
approvals.agents = agents;
|
|
405
|
+
try {
|
|
406
|
+
saveApprovals(approvals);
|
|
407
|
+
logger.info({ src: "exec-approval", pattern: trimmed, agentId: target }, "Added pattern to allowlist");
|
|
408
|
+
return true;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
logger.error({ src: "exec-approval", error, pattern: trimmed }, "Failed to save allowlist after adding entry");
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function minSecurity(a, b) {
|
|
415
|
+
const order = {
|
|
416
|
+
deny: 0,
|
|
417
|
+
allowlist: 1,
|
|
418
|
+
full: 2
|
|
419
|
+
};
|
|
420
|
+
return order[a] <= order[b] ? a : b;
|
|
421
|
+
}
|
|
422
|
+
function maxAsk(a, b) {
|
|
423
|
+
const order = { off: 0, "on-miss": 1, always: 2 };
|
|
424
|
+
return order[a] >= order[b] ? a : b;
|
|
425
|
+
}
|
|
426
|
+
// approvals/analysis.ts
|
|
427
|
+
import fs2 from "node:fs";
|
|
428
|
+
import path2 from "node:path";
|
|
429
|
+
var DISALLOWED_PIPELINE_TOKENS = new Set([">", "<", "`", `
|
|
430
|
+
`, "\r", "(", ")"]);
|
|
431
|
+
var DOUBLE_QUOTE_ESCAPES = new Set(["\\", '"', "$", "`", `
|
|
432
|
+
`, "\r"]);
|
|
433
|
+
var WINDOWS_UNSUPPORTED_TOKENS = new Set([
|
|
434
|
+
"&",
|
|
435
|
+
"|",
|
|
436
|
+
"<",
|
|
437
|
+
">",
|
|
438
|
+
"^",
|
|
439
|
+
"(",
|
|
440
|
+
")",
|
|
441
|
+
"%",
|
|
442
|
+
"!",
|
|
443
|
+
`
|
|
444
|
+
`,
|
|
445
|
+
"\r"
|
|
446
|
+
]);
|
|
447
|
+
function isDoubleQuoteEscape(next) {
|
|
448
|
+
return Boolean(next && DOUBLE_QUOTE_ESCAPES.has(next));
|
|
449
|
+
}
|
|
450
|
+
function isExecutableFile(filePath) {
|
|
451
|
+
try {
|
|
452
|
+
const stat = fs2.statSync(filePath);
|
|
453
|
+
if (!stat.isFile())
|
|
454
|
+
return false;
|
|
455
|
+
if (process.platform !== "win32") {
|
|
456
|
+
fs2.accessSync(filePath, fs2.constants.X_OK);
|
|
457
|
+
}
|
|
458
|
+
return true;
|
|
459
|
+
} catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function expandHome2(value) {
|
|
464
|
+
if (!value)
|
|
465
|
+
return value;
|
|
466
|
+
if (value === "~")
|
|
467
|
+
return __require("node:os").homedir();
|
|
468
|
+
if (value.startsWith("~/"))
|
|
469
|
+
return path2.join(__require("node:os").homedir(), value.slice(2));
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
function parseFirstToken(command) {
|
|
473
|
+
const trimmed = command.trim();
|
|
474
|
+
if (!trimmed)
|
|
475
|
+
return null;
|
|
476
|
+
const first = trimmed[0];
|
|
477
|
+
if (first === '"' || first === "'") {
|
|
478
|
+
const end = trimmed.indexOf(first, 1);
|
|
479
|
+
if (end > 1)
|
|
480
|
+
return trimmed.slice(1, end);
|
|
481
|
+
return trimmed.slice(1);
|
|
482
|
+
}
|
|
483
|
+
const match = /^[^\s]+/.exec(trimmed);
|
|
484
|
+
return match ? match[0] : null;
|
|
485
|
+
}
|
|
486
|
+
function resolveExecutablePath(rawExecutable, cwd, env) {
|
|
487
|
+
const expanded = rawExecutable.startsWith("~") ? expandHome2(rawExecutable) : rawExecutable;
|
|
488
|
+
if (expanded.includes("/") || expanded.includes("\\")) {
|
|
489
|
+
if (path2.isAbsolute(expanded)) {
|
|
490
|
+
return isExecutableFile(expanded) ? expanded : undefined;
|
|
491
|
+
}
|
|
492
|
+
const base = cwd?.trim() || process.cwd();
|
|
493
|
+
const candidate = path2.resolve(base, expanded);
|
|
494
|
+
return isExecutableFile(candidate) ? candidate : undefined;
|
|
495
|
+
}
|
|
496
|
+
const envPath = env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path ?? "";
|
|
497
|
+
const entries = envPath.split(path2.delimiter).filter(Boolean);
|
|
498
|
+
const hasExtension = process.platform === "win32" && path2.extname(expanded).length > 0;
|
|
499
|
+
const extensions = process.platform === "win32" ? hasExtension ? [""] : (env?.PATHEXT ?? env?.Pathext ?? process.env.PATHEXT ?? process.env.Pathext ?? ".EXE;.CMD;.BAT;.COM").split(";").map((ext) => ext.toLowerCase()) : [""];
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
for (const ext of extensions) {
|
|
502
|
+
const candidate = path2.join(entry, expanded + ext);
|
|
503
|
+
if (isExecutableFile(candidate)) {
|
|
504
|
+
return candidate;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
function resolveCommandResolution(command, cwd, env) {
|
|
511
|
+
const rawExecutable = parseFirstToken(command);
|
|
512
|
+
if (!rawExecutable)
|
|
513
|
+
return null;
|
|
514
|
+
const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env);
|
|
515
|
+
const executableName = resolvedPath ? path2.basename(resolvedPath) : rawExecutable;
|
|
516
|
+
return { rawExecutable, resolvedPath, executableName };
|
|
517
|
+
}
|
|
518
|
+
function resolveCommandFromArgv(argv, cwd, env) {
|
|
519
|
+
const rawExecutable = argv[0]?.trim();
|
|
520
|
+
if (!rawExecutable)
|
|
521
|
+
return null;
|
|
522
|
+
const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env);
|
|
523
|
+
const executableName = resolvedPath ? path2.basename(resolvedPath) : rawExecutable;
|
|
524
|
+
return { rawExecutable, resolvedPath, executableName };
|
|
525
|
+
}
|
|
526
|
+
function iterateQuoteAware(command, onChar) {
|
|
527
|
+
const parts = [];
|
|
528
|
+
let buf = "";
|
|
529
|
+
let inSingle = false;
|
|
530
|
+
let inDouble = false;
|
|
531
|
+
let escaped = false;
|
|
532
|
+
let hasSplit = false;
|
|
533
|
+
const pushPart = () => {
|
|
534
|
+
const trimmed = buf.trim();
|
|
535
|
+
if (trimmed)
|
|
536
|
+
parts.push(trimmed);
|
|
537
|
+
buf = "";
|
|
538
|
+
};
|
|
539
|
+
for (let i = 0;i < command.length; i += 1) {
|
|
540
|
+
const ch = command[i];
|
|
541
|
+
const next = command[i + 1];
|
|
542
|
+
if (escaped) {
|
|
543
|
+
buf += ch;
|
|
544
|
+
escaped = false;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!inSingle && !inDouble && ch === "\\") {
|
|
548
|
+
escaped = true;
|
|
549
|
+
buf += ch;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (inSingle) {
|
|
553
|
+
if (ch === "'")
|
|
554
|
+
inSingle = false;
|
|
555
|
+
buf += ch;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (inDouble) {
|
|
559
|
+
if (ch === "\\" && isDoubleQuoteEscape(next)) {
|
|
560
|
+
buf += ch;
|
|
561
|
+
buf += next;
|
|
562
|
+
i += 1;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (ch === "$" && next === "(") {
|
|
566
|
+
return { ok: false, reason: "unsupported shell token: $()" };
|
|
567
|
+
}
|
|
568
|
+
if (ch === "`") {
|
|
569
|
+
return { ok: false, reason: "unsupported shell token: `" };
|
|
570
|
+
}
|
|
571
|
+
if (ch === `
|
|
572
|
+
` || ch === "\r") {
|
|
573
|
+
return { ok: false, reason: "unsupported shell token: newline" };
|
|
574
|
+
}
|
|
575
|
+
if (ch === '"')
|
|
576
|
+
inDouble = false;
|
|
577
|
+
buf += ch;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (ch === "'") {
|
|
581
|
+
inSingle = true;
|
|
582
|
+
buf += ch;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (ch === '"') {
|
|
586
|
+
inDouble = true;
|
|
587
|
+
buf += ch;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const action = onChar(ch, next, i);
|
|
591
|
+
if (typeof action === "object" && "reject" in action) {
|
|
592
|
+
return { ok: false, reason: action.reject };
|
|
593
|
+
}
|
|
594
|
+
if (action === "split") {
|
|
595
|
+
pushPart();
|
|
596
|
+
hasSplit = true;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (action === "skip")
|
|
600
|
+
continue;
|
|
601
|
+
buf += ch;
|
|
602
|
+
}
|
|
603
|
+
if (escaped || inSingle || inDouble) {
|
|
604
|
+
return { ok: false, reason: "unterminated shell quote/escape" };
|
|
605
|
+
}
|
|
606
|
+
pushPart();
|
|
607
|
+
return { ok: true, parts, hasSplit };
|
|
608
|
+
}
|
|
609
|
+
function splitShellPipeline(command) {
|
|
610
|
+
let emptySegment = false;
|
|
611
|
+
const result = iterateQuoteAware(command, (ch, next) => {
|
|
612
|
+
if (ch === "|" && next === "|") {
|
|
613
|
+
return { reject: "unsupported shell token: ||" };
|
|
614
|
+
}
|
|
615
|
+
if (ch === "|" && next === "&") {
|
|
616
|
+
return { reject: "unsupported shell token: |&" };
|
|
617
|
+
}
|
|
618
|
+
if (ch === "|") {
|
|
619
|
+
emptySegment = true;
|
|
620
|
+
return "split";
|
|
621
|
+
}
|
|
622
|
+
if (ch === "&" || ch === ";") {
|
|
623
|
+
return { reject: `unsupported shell token: ${ch}` };
|
|
624
|
+
}
|
|
625
|
+
if (DISALLOWED_PIPELINE_TOKENS.has(ch)) {
|
|
626
|
+
return { reject: `unsupported shell token: ${ch}` };
|
|
627
|
+
}
|
|
628
|
+
if (ch === "$" && next === "(") {
|
|
629
|
+
return { reject: "unsupported shell token: $()" };
|
|
630
|
+
}
|
|
631
|
+
emptySegment = false;
|
|
632
|
+
return "include";
|
|
633
|
+
});
|
|
634
|
+
if (!result.ok) {
|
|
635
|
+
return { ok: false, reason: result.reason, segments: [] };
|
|
636
|
+
}
|
|
637
|
+
if (emptySegment || result.parts.length === 0) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
reason: result.parts.length === 0 ? "empty command" : "empty pipeline segment",
|
|
641
|
+
segments: []
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return { ok: true, segments: result.parts };
|
|
645
|
+
}
|
|
646
|
+
function tokenizeShellSegment(segment) {
|
|
647
|
+
const tokens = [];
|
|
648
|
+
let buf = "";
|
|
649
|
+
let inSingle = false;
|
|
650
|
+
let inDouble = false;
|
|
651
|
+
let escaped = false;
|
|
652
|
+
const pushToken = () => {
|
|
653
|
+
if (buf.length > 0) {
|
|
654
|
+
tokens.push(buf);
|
|
655
|
+
buf = "";
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
for (let i = 0;i < segment.length; i += 1) {
|
|
659
|
+
const ch = segment[i];
|
|
660
|
+
if (escaped) {
|
|
661
|
+
buf += ch;
|
|
662
|
+
escaped = false;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (!inSingle && !inDouble && ch === "\\") {
|
|
666
|
+
escaped = true;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (inSingle) {
|
|
670
|
+
if (ch === "'")
|
|
671
|
+
inSingle = false;
|
|
672
|
+
else
|
|
673
|
+
buf += ch;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (inDouble) {
|
|
677
|
+
const next = segment[i + 1];
|
|
678
|
+
if (ch === "\\" && isDoubleQuoteEscape(next)) {
|
|
679
|
+
buf += next;
|
|
680
|
+
i += 1;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (ch === '"')
|
|
684
|
+
inDouble = false;
|
|
685
|
+
else
|
|
686
|
+
buf += ch;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (ch === "'") {
|
|
690
|
+
inSingle = true;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (ch === '"') {
|
|
694
|
+
inDouble = true;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (/\s/.test(ch)) {
|
|
698
|
+
pushToken();
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
buf += ch;
|
|
702
|
+
}
|
|
703
|
+
if (escaped || inSingle || inDouble)
|
|
704
|
+
return null;
|
|
705
|
+
pushToken();
|
|
706
|
+
return tokens;
|
|
707
|
+
}
|
|
708
|
+
function parseSegmentsFromParts(parts, cwd, env) {
|
|
709
|
+
const segments = [];
|
|
710
|
+
for (const raw of parts) {
|
|
711
|
+
const argv = tokenizeShellSegment(raw);
|
|
712
|
+
if (!argv || argv.length === 0)
|
|
713
|
+
return null;
|
|
714
|
+
segments.push({
|
|
715
|
+
raw,
|
|
716
|
+
argv,
|
|
717
|
+
resolution: resolveCommandFromArgv(argv, cwd, env)
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return segments;
|
|
721
|
+
}
|
|
722
|
+
function isWindowsPlatform(platform) {
|
|
723
|
+
const normalized = String(platform ?? "").trim().toLowerCase();
|
|
724
|
+
return normalized.startsWith("win");
|
|
725
|
+
}
|
|
726
|
+
function analyzeShellCommand(params) {
|
|
727
|
+
if (isWindowsPlatform(params.platform)) {
|
|
728
|
+
return analyzeWindowsCommand(params);
|
|
729
|
+
}
|
|
730
|
+
const chainParts = splitCommandChain(params.command);
|
|
731
|
+
if (chainParts) {
|
|
732
|
+
const chains = [];
|
|
733
|
+
const allSegments = [];
|
|
734
|
+
for (const part of chainParts) {
|
|
735
|
+
const pipelineSplit = splitShellPipeline(part);
|
|
736
|
+
if (!pipelineSplit.ok) {
|
|
737
|
+
return { ok: false, reason: pipelineSplit.reason, segments: [] };
|
|
738
|
+
}
|
|
739
|
+
const segments2 = parseSegmentsFromParts(pipelineSplit.segments, params.cwd, params.env);
|
|
740
|
+
if (!segments2) {
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
reason: "unable to parse shell segment",
|
|
744
|
+
segments: []
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
chains.push(segments2);
|
|
748
|
+
allSegments.push(...segments2);
|
|
749
|
+
}
|
|
750
|
+
return { ok: true, segments: allSegments, chains };
|
|
751
|
+
}
|
|
752
|
+
const split = splitShellPipeline(params.command);
|
|
753
|
+
if (!split.ok) {
|
|
754
|
+
return { ok: false, reason: split.reason, segments: [] };
|
|
755
|
+
}
|
|
756
|
+
const segments = parseSegmentsFromParts(split.segments, params.cwd, params.env);
|
|
757
|
+
if (!segments) {
|
|
758
|
+
return { ok: false, reason: "unable to parse shell segment", segments: [] };
|
|
759
|
+
}
|
|
760
|
+
return { ok: true, segments };
|
|
761
|
+
}
|
|
762
|
+
function analyzeWindowsCommand(params) {
|
|
763
|
+
for (const ch of params.command) {
|
|
764
|
+
if (WINDOWS_UNSUPPORTED_TOKENS.has(ch)) {
|
|
765
|
+
const tokenName = ch === `
|
|
766
|
+
` || ch === "\r" ? "newline" : ch;
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
reason: `unsupported windows shell token: ${tokenName}`,
|
|
770
|
+
segments: []
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const argv = tokenizeWindowsSegment(params.command);
|
|
775
|
+
if (!argv || argv.length === 0) {
|
|
776
|
+
return {
|
|
777
|
+
ok: false,
|
|
778
|
+
reason: "unable to parse windows command",
|
|
779
|
+
segments: []
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
ok: true,
|
|
784
|
+
segments: [
|
|
785
|
+
{
|
|
786
|
+
raw: params.command,
|
|
787
|
+
argv,
|
|
788
|
+
resolution: resolveCommandFromArgv(argv, params.cwd, params.env)
|
|
789
|
+
}
|
|
790
|
+
]
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function tokenizeWindowsSegment(segment) {
|
|
794
|
+
const tokens = [];
|
|
795
|
+
let buf = "";
|
|
796
|
+
let inDouble = false;
|
|
797
|
+
const pushToken = () => {
|
|
798
|
+
if (buf.length > 0) {
|
|
799
|
+
tokens.push(buf);
|
|
800
|
+
buf = "";
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
for (const ch of segment) {
|
|
804
|
+
if (ch === '"') {
|
|
805
|
+
inDouble = !inDouble;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (!inDouble && /\s/.test(ch)) {
|
|
809
|
+
pushToken();
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
buf += ch;
|
|
813
|
+
}
|
|
814
|
+
if (inDouble)
|
|
815
|
+
return null;
|
|
816
|
+
pushToken();
|
|
817
|
+
return tokens.length > 0 ? tokens : null;
|
|
818
|
+
}
|
|
819
|
+
function splitCommandChain(command) {
|
|
820
|
+
const parts = [];
|
|
821
|
+
let buf = "";
|
|
822
|
+
let inSingle = false;
|
|
823
|
+
let inDouble = false;
|
|
824
|
+
let escaped = false;
|
|
825
|
+
let foundChain = false;
|
|
826
|
+
let invalidChain = false;
|
|
827
|
+
const pushPart = () => {
|
|
828
|
+
const trimmed = buf.trim();
|
|
829
|
+
if (trimmed) {
|
|
830
|
+
parts.push(trimmed);
|
|
831
|
+
buf = "";
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
buf = "";
|
|
835
|
+
return false;
|
|
836
|
+
};
|
|
837
|
+
for (let i = 0;i < command.length; i += 1) {
|
|
838
|
+
const ch = command[i];
|
|
839
|
+
const next = command[i + 1];
|
|
840
|
+
if (escaped) {
|
|
841
|
+
buf += ch;
|
|
842
|
+
escaped = false;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (!inSingle && !inDouble && ch === "\\") {
|
|
846
|
+
escaped = true;
|
|
847
|
+
buf += ch;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (inSingle) {
|
|
851
|
+
if (ch === "'")
|
|
852
|
+
inSingle = false;
|
|
853
|
+
buf += ch;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (inDouble) {
|
|
857
|
+
if (ch === "\\" && isDoubleQuoteEscape(next)) {
|
|
858
|
+
buf += ch;
|
|
859
|
+
buf += next;
|
|
860
|
+
i += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (ch === '"')
|
|
864
|
+
inDouble = false;
|
|
865
|
+
buf += ch;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (ch === "'") {
|
|
869
|
+
inSingle = true;
|
|
870
|
+
buf += ch;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (ch === '"') {
|
|
874
|
+
inDouble = true;
|
|
875
|
+
buf += ch;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (ch === "&" && next === "&") {
|
|
879
|
+
if (!pushPart())
|
|
880
|
+
invalidChain = true;
|
|
881
|
+
i += 1;
|
|
882
|
+
foundChain = true;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (ch === "|" && next === "|") {
|
|
886
|
+
if (!pushPart())
|
|
887
|
+
invalidChain = true;
|
|
888
|
+
i += 1;
|
|
889
|
+
foundChain = true;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (ch === ";") {
|
|
893
|
+
if (!pushPart())
|
|
894
|
+
invalidChain = true;
|
|
895
|
+
foundChain = true;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
buf += ch;
|
|
899
|
+
}
|
|
900
|
+
const pushedFinal = pushPart();
|
|
901
|
+
if (!foundChain)
|
|
902
|
+
return null;
|
|
903
|
+
if (invalidChain || !pushedFinal)
|
|
904
|
+
return null;
|
|
905
|
+
return parts.length > 0 ? parts : null;
|
|
906
|
+
}
|
|
907
|
+
function normalizeSafeBins(entries) {
|
|
908
|
+
if (!Array.isArray(entries))
|
|
909
|
+
return new Set;
|
|
910
|
+
const normalized = entries.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
911
|
+
return new Set(normalized);
|
|
912
|
+
}
|
|
913
|
+
function resolveSafeBins(entries) {
|
|
914
|
+
if (entries === undefined) {
|
|
915
|
+
return normalizeSafeBins([...DEFAULT_SAFE_BINS]);
|
|
916
|
+
}
|
|
917
|
+
return normalizeSafeBins(entries ?? []);
|
|
918
|
+
}
|
|
919
|
+
function isPathLikeToken(value) {
|
|
920
|
+
const trimmed = value.trim();
|
|
921
|
+
if (!trimmed || trimmed === "-")
|
|
922
|
+
return false;
|
|
923
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~"))
|
|
924
|
+
return true;
|
|
925
|
+
if (trimmed.startsWith("/"))
|
|
926
|
+
return true;
|
|
927
|
+
return /^[A-Za-z]:[\\/]/.test(trimmed);
|
|
928
|
+
}
|
|
929
|
+
function defaultFileExists(filePath) {
|
|
930
|
+
try {
|
|
931
|
+
return fs2.existsSync(filePath);
|
|
932
|
+
} catch {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function isSafeBinUsage(params) {
|
|
937
|
+
if (params.safeBins.size === 0)
|
|
938
|
+
return false;
|
|
939
|
+
const resolution = params.resolution;
|
|
940
|
+
const execName = resolution?.executableName?.toLowerCase();
|
|
941
|
+
if (!execName)
|
|
942
|
+
return false;
|
|
943
|
+
const matchesSafeBin = params.safeBins.has(execName) || process.platform === "win32" && params.safeBins.has(path2.parse(execName).name);
|
|
944
|
+
if (!matchesSafeBin)
|
|
945
|
+
return false;
|
|
946
|
+
if (!resolution?.resolvedPath)
|
|
947
|
+
return false;
|
|
948
|
+
const cwd = params.cwd ?? process.cwd();
|
|
949
|
+
const exists = params.fileExists ?? defaultFileExists;
|
|
950
|
+
const argv = params.argv.slice(1);
|
|
951
|
+
for (const token of argv) {
|
|
952
|
+
if (!token || token === "-")
|
|
953
|
+
continue;
|
|
954
|
+
if (token.startsWith("-")) {
|
|
955
|
+
const eqIndex = token.indexOf("=");
|
|
956
|
+
if (eqIndex > 0) {
|
|
957
|
+
const value = token.slice(eqIndex + 1);
|
|
958
|
+
if (value && (isPathLikeToken(value) || exists(path2.resolve(cwd, value)))) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (isPathLikeToken(token))
|
|
965
|
+
return false;
|
|
966
|
+
if (exists(path2.resolve(cwd, token)))
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
function resolveAllowlistCandidatePath(resolution, cwd) {
|
|
972
|
+
if (!resolution)
|
|
973
|
+
return;
|
|
974
|
+
if (resolution.resolvedPath)
|
|
975
|
+
return resolution.resolvedPath;
|
|
976
|
+
const raw = resolution.rawExecutable.trim();
|
|
977
|
+
if (!raw)
|
|
978
|
+
return;
|
|
979
|
+
const expanded = raw.startsWith("~") ? expandHome2(raw) : raw;
|
|
980
|
+
if (!expanded.includes("/") && !expanded.includes("\\"))
|
|
981
|
+
return;
|
|
982
|
+
if (path2.isAbsolute(expanded))
|
|
983
|
+
return expanded;
|
|
984
|
+
const base = cwd?.trim() || process.cwd();
|
|
985
|
+
return path2.resolve(base, expanded);
|
|
986
|
+
}
|
|
987
|
+
function evaluateSegments(segments, params) {
|
|
988
|
+
const matches = [];
|
|
989
|
+
const allowSkills = params.autoAllowSkills === true && (params.skillBins?.size ?? 0) > 0;
|
|
990
|
+
const satisfied = segments.every((segment) => {
|
|
991
|
+
const candidatePath = resolveAllowlistCandidatePath(segment.resolution, params.cwd);
|
|
992
|
+
const candidateResolution = candidatePath && segment.resolution ? { ...segment.resolution, resolvedPath: candidatePath } : segment.resolution;
|
|
993
|
+
const match = matchAllowlist(params.allowlist, candidateResolution);
|
|
994
|
+
if (match)
|
|
995
|
+
matches.push(match);
|
|
996
|
+
const safe = isSafeBinUsage({
|
|
997
|
+
argv: segment.argv,
|
|
998
|
+
resolution: segment.resolution,
|
|
999
|
+
safeBins: params.safeBins,
|
|
1000
|
+
cwd: params.cwd
|
|
1001
|
+
});
|
|
1002
|
+
const skillAllow = allowSkills && segment.resolution?.executableName ? params.skillBins?.has(segment.resolution.executableName) : false;
|
|
1003
|
+
return Boolean(match || safe || skillAllow);
|
|
1004
|
+
});
|
|
1005
|
+
return { satisfied, matches };
|
|
1006
|
+
}
|
|
1007
|
+
function evaluateExecAllowlist(params) {
|
|
1008
|
+
const allowlistMatches = [];
|
|
1009
|
+
if (!params.analysis.ok || params.analysis.segments.length === 0) {
|
|
1010
|
+
return { allowlistSatisfied: false, allowlistMatches };
|
|
1011
|
+
}
|
|
1012
|
+
if (params.analysis.chains) {
|
|
1013
|
+
for (const chainSegments of params.analysis.chains) {
|
|
1014
|
+
const result2 = evaluateSegments(chainSegments, {
|
|
1015
|
+
allowlist: params.allowlist,
|
|
1016
|
+
safeBins: params.safeBins,
|
|
1017
|
+
cwd: params.cwd,
|
|
1018
|
+
skillBins: params.skillBins,
|
|
1019
|
+
autoAllowSkills: params.autoAllowSkills
|
|
1020
|
+
});
|
|
1021
|
+
if (!result2.satisfied) {
|
|
1022
|
+
return { allowlistSatisfied: false, allowlistMatches: [] };
|
|
1023
|
+
}
|
|
1024
|
+
allowlistMatches.push(...result2.matches);
|
|
1025
|
+
}
|
|
1026
|
+
return { allowlistSatisfied: true, allowlistMatches };
|
|
1027
|
+
}
|
|
1028
|
+
const result = evaluateSegments(params.analysis.segments, {
|
|
1029
|
+
allowlist: params.allowlist,
|
|
1030
|
+
safeBins: params.safeBins,
|
|
1031
|
+
cwd: params.cwd,
|
|
1032
|
+
skillBins: params.skillBins,
|
|
1033
|
+
autoAllowSkills: params.autoAllowSkills
|
|
1034
|
+
});
|
|
1035
|
+
return {
|
|
1036
|
+
allowlistSatisfied: result.satisfied,
|
|
1037
|
+
allowlistMatches: result.matches
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function evaluateShellAllowlist(params) {
|
|
1041
|
+
const chainParts = isWindowsPlatform(params.platform) ? null : splitCommandChain(params.command);
|
|
1042
|
+
if (!chainParts) {
|
|
1043
|
+
const analysis = analyzeShellCommand({
|
|
1044
|
+
command: params.command,
|
|
1045
|
+
cwd: params.cwd,
|
|
1046
|
+
env: params.env,
|
|
1047
|
+
platform: params.platform
|
|
1048
|
+
});
|
|
1049
|
+
if (!analysis.ok) {
|
|
1050
|
+
return {
|
|
1051
|
+
analysisOk: false,
|
|
1052
|
+
allowlistSatisfied: false,
|
|
1053
|
+
allowlistMatches: [],
|
|
1054
|
+
segments: []
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
const evaluation = evaluateExecAllowlist({
|
|
1058
|
+
analysis,
|
|
1059
|
+
allowlist: params.allowlist,
|
|
1060
|
+
safeBins: params.safeBins,
|
|
1061
|
+
cwd: params.cwd,
|
|
1062
|
+
skillBins: params.skillBins,
|
|
1063
|
+
autoAllowSkills: params.autoAllowSkills
|
|
1064
|
+
});
|
|
1065
|
+
return {
|
|
1066
|
+
analysisOk: true,
|
|
1067
|
+
allowlistSatisfied: evaluation.allowlistSatisfied,
|
|
1068
|
+
allowlistMatches: evaluation.allowlistMatches,
|
|
1069
|
+
segments: analysis.segments
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
const allowlistMatches = [];
|
|
1073
|
+
const segments = [];
|
|
1074
|
+
for (const part of chainParts) {
|
|
1075
|
+
const analysis = analyzeShellCommand({
|
|
1076
|
+
command: part,
|
|
1077
|
+
cwd: params.cwd,
|
|
1078
|
+
env: params.env,
|
|
1079
|
+
platform: params.platform
|
|
1080
|
+
});
|
|
1081
|
+
if (!analysis.ok) {
|
|
1082
|
+
return {
|
|
1083
|
+
analysisOk: false,
|
|
1084
|
+
allowlistSatisfied: false,
|
|
1085
|
+
allowlistMatches: [],
|
|
1086
|
+
segments: []
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
segments.push(...analysis.segments);
|
|
1090
|
+
const evaluation = evaluateExecAllowlist({
|
|
1091
|
+
analysis,
|
|
1092
|
+
allowlist: params.allowlist,
|
|
1093
|
+
safeBins: params.safeBins,
|
|
1094
|
+
cwd: params.cwd,
|
|
1095
|
+
skillBins: params.skillBins,
|
|
1096
|
+
autoAllowSkills: params.autoAllowSkills
|
|
1097
|
+
});
|
|
1098
|
+
allowlistMatches.push(...evaluation.allowlistMatches);
|
|
1099
|
+
if (!evaluation.allowlistSatisfied) {
|
|
1100
|
+
return {
|
|
1101
|
+
analysisOk: true,
|
|
1102
|
+
allowlistSatisfied: false,
|
|
1103
|
+
allowlistMatches,
|
|
1104
|
+
segments
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
analysisOk: true,
|
|
1110
|
+
allowlistSatisfied: true,
|
|
1111
|
+
allowlistMatches,
|
|
1112
|
+
segments
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function requiresExecApproval(params) {
|
|
1116
|
+
return params.ask === "always" || params.ask === "on-miss" && params.security === "allowlist" && (!params.analysisOk || !params.allowlistSatisfied);
|
|
1117
|
+
}
|
|
1118
|
+
// approvals/service.ts
|
|
1119
|
+
import { logger as logger2, Service } from "@elizaos/core";
|
|
1120
|
+
var EXEC_APPROVAL_OPTIONS = [
|
|
1121
|
+
{ name: "allow-once", description: "Allow this one time" },
|
|
1122
|
+
{ name: "allow-always", description: "Always allow this" },
|
|
1123
|
+
{ name: "deny", description: "Deny the request", isCancel: true }
|
|
1124
|
+
];
|
|
1125
|
+
|
|
1126
|
+
class ExecApprovalService extends Service {
|
|
1127
|
+
static serviceType = "exec_approval";
|
|
1128
|
+
capabilityDescription = "Manages command execution approvals with allowlist and user confirmation";
|
|
1129
|
+
approvalConfig = null;
|
|
1130
|
+
safeBins;
|
|
1131
|
+
skillBins;
|
|
1132
|
+
constructor(runtime) {
|
|
1133
|
+
super(runtime);
|
|
1134
|
+
this.safeBins = resolveSafeBins();
|
|
1135
|
+
this.skillBins = new Set;
|
|
1136
|
+
}
|
|
1137
|
+
static async start(runtime) {
|
|
1138
|
+
const service = new ExecApprovalService(runtime);
|
|
1139
|
+
try {
|
|
1140
|
+
service.approvalConfig = resolveApprovals(runtime.agentId);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
logger2.error({ src: "service:exec_approval", error, agentId: runtime.agentId }, "Failed to load approval config during startup - using in-memory defaults. " + "Approvals may not persist. Check state-dir file permissions.");
|
|
1143
|
+
service.approvalConfig = {
|
|
1144
|
+
path: "",
|
|
1145
|
+
socketPath: "",
|
|
1146
|
+
token: "",
|
|
1147
|
+
defaults: {
|
|
1148
|
+
security: "deny",
|
|
1149
|
+
ask: "on-miss",
|
|
1150
|
+
askFallback: "deny",
|
|
1151
|
+
autoAllowSkills: false
|
|
1152
|
+
},
|
|
1153
|
+
agent: {
|
|
1154
|
+
security: "deny",
|
|
1155
|
+
ask: "on-miss",
|
|
1156
|
+
askFallback: "deny",
|
|
1157
|
+
autoAllowSkills: false
|
|
1158
|
+
},
|
|
1159
|
+
allowlist: [],
|
|
1160
|
+
file: { version: 1, agents: {} }
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
logger2.info({ src: "service:exec_approval", agentId: runtime.agentId }, "ExecApprovalService started");
|
|
1164
|
+
return service;
|
|
1165
|
+
}
|
|
1166
|
+
async stop() {
|
|
1167
|
+
logger2.debug({ src: "service:exec_approval" }, "ExecApprovalService stopped");
|
|
1168
|
+
}
|
|
1169
|
+
loadConfig(agentId) {
|
|
1170
|
+
this.approvalConfig = resolveApprovals(agentId ?? this.runtime.agentId);
|
|
1171
|
+
return this.approvalConfig;
|
|
1172
|
+
}
|
|
1173
|
+
getConfig() {
|
|
1174
|
+
if (!this.approvalConfig) {
|
|
1175
|
+
this.approvalConfig = resolveApprovals(this.runtime.agentId);
|
|
1176
|
+
}
|
|
1177
|
+
return this.approvalConfig;
|
|
1178
|
+
}
|
|
1179
|
+
setSafeBins(bins) {
|
|
1180
|
+
this.safeBins = resolveSafeBins(bins);
|
|
1181
|
+
}
|
|
1182
|
+
setSkillBins(bins) {
|
|
1183
|
+
this.skillBins = new Set(bins.map((b) => b.toLowerCase()));
|
|
1184
|
+
}
|
|
1185
|
+
async checkCommand(params) {
|
|
1186
|
+
const config = this.getConfig();
|
|
1187
|
+
const analysis = analyzeShellCommand({
|
|
1188
|
+
command: params.command,
|
|
1189
|
+
cwd: params.cwd,
|
|
1190
|
+
env: params.env
|
|
1191
|
+
});
|
|
1192
|
+
if (!analysis.ok) {
|
|
1193
|
+
return {
|
|
1194
|
+
allowed: false,
|
|
1195
|
+
requiresApproval: false,
|
|
1196
|
+
reason: analysis.reason ?? "Command analysis failed",
|
|
1197
|
+
analysis,
|
|
1198
|
+
allowlistMatches: []
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
const evaluation = evaluateShellAllowlist({
|
|
1202
|
+
command: params.command,
|
|
1203
|
+
allowlist: config.allowlist,
|
|
1204
|
+
safeBins: this.safeBins,
|
|
1205
|
+
cwd: params.cwd,
|
|
1206
|
+
env: params.env,
|
|
1207
|
+
skillBins: this.skillBins,
|
|
1208
|
+
autoAllowSkills: config.agent.autoAllowSkills
|
|
1209
|
+
});
|
|
1210
|
+
const security = config.agent.security;
|
|
1211
|
+
const ask = config.agent.ask;
|
|
1212
|
+
if (security === "full") {
|
|
1213
|
+
return {
|
|
1214
|
+
allowed: true,
|
|
1215
|
+
requiresApproval: false,
|
|
1216
|
+
analysis,
|
|
1217
|
+
allowlistMatches: evaluation.allowlistMatches
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
if (security === "deny") {
|
|
1221
|
+
return {
|
|
1222
|
+
allowed: false,
|
|
1223
|
+
requiresApproval: false,
|
|
1224
|
+
reason: "Command execution is disabled",
|
|
1225
|
+
analysis,
|
|
1226
|
+
allowlistMatches: []
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if (evaluation.allowlistSatisfied) {
|
|
1230
|
+
let recordingFailed = false;
|
|
1231
|
+
for (const match of evaluation.allowlistMatches) {
|
|
1232
|
+
const approvals = loadApprovals();
|
|
1233
|
+
const recorded = recordAllowlistUse(approvals, params.agentId ?? this.runtime.agentId, match, params.command, analysis.segments[0]?.resolution?.resolvedPath);
|
|
1234
|
+
if (!recorded) {
|
|
1235
|
+
recordingFailed = true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (recordingFailed) {
|
|
1239
|
+
logger2.debug({ src: "service:exec_approval", command: params.command }, "Some allowlist usage records failed to save - command will still proceed");
|
|
1240
|
+
}
|
|
1241
|
+
return {
|
|
1242
|
+
allowed: true,
|
|
1243
|
+
requiresApproval: false,
|
|
1244
|
+
analysis,
|
|
1245
|
+
allowlistMatches: evaluation.allowlistMatches
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const needsApproval = requiresExecApproval({
|
|
1249
|
+
ask,
|
|
1250
|
+
security,
|
|
1251
|
+
analysisOk: analysis.ok,
|
|
1252
|
+
allowlistSatisfied: evaluation.allowlistSatisfied
|
|
1253
|
+
});
|
|
1254
|
+
if (!needsApproval) {
|
|
1255
|
+
const fallback = config.agent.askFallback;
|
|
1256
|
+
if (fallback === "deny") {
|
|
1257
|
+
return {
|
|
1258
|
+
allowed: false,
|
|
1259
|
+
requiresApproval: false,
|
|
1260
|
+
reason: "Command not in allowlist",
|
|
1261
|
+
analysis,
|
|
1262
|
+
allowlistMatches: []
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
allowed: true,
|
|
1267
|
+
requiresApproval: false,
|
|
1268
|
+
analysis,
|
|
1269
|
+
allowlistMatches: []
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
const requestId = crypto.randomUUID();
|
|
1273
|
+
const request = {
|
|
1274
|
+
id: requestId,
|
|
1275
|
+
command: params.command,
|
|
1276
|
+
cwd: params.cwd,
|
|
1277
|
+
security,
|
|
1278
|
+
ask,
|
|
1279
|
+
agentId: params.agentId ?? this.runtime.agentId,
|
|
1280
|
+
resolvedPath: analysis.segments[0]?.resolution?.resolvedPath,
|
|
1281
|
+
roomId: params.roomId,
|
|
1282
|
+
timeoutMs: EXEC_APPROVAL_DEFAULTS.timeoutMs
|
|
1283
|
+
};
|
|
1284
|
+
return {
|
|
1285
|
+
allowed: false,
|
|
1286
|
+
requiresApproval: true,
|
|
1287
|
+
request,
|
|
1288
|
+
analysis,
|
|
1289
|
+
allowlistMatches: []
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
async requestApproval(request) {
|
|
1293
|
+
const approvalService = this.runtime.getService("approval");
|
|
1294
|
+
if (!approvalService) {
|
|
1295
|
+
logger2.warn({ src: "service:exec_approval" }, "ApprovalService not available, denying by default");
|
|
1296
|
+
return {
|
|
1297
|
+
decision: "deny",
|
|
1298
|
+
timedOut: false
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const descriptionLines = ["**Exec Approval Required**", "", `Command: \`${request.command}\``];
|
|
1302
|
+
if (request.cwd) {
|
|
1303
|
+
descriptionLines.push(`CWD: \`${request.cwd}\``);
|
|
1304
|
+
}
|
|
1305
|
+
if (request.resolvedPath) {
|
|
1306
|
+
descriptionLines.push(`Executable: \`${request.resolvedPath}\``);
|
|
1307
|
+
}
|
|
1308
|
+
const description = descriptionLines.join(`
|
|
1309
|
+
`);
|
|
1310
|
+
const result = await approvalService.requestApproval({
|
|
1311
|
+
name: "EXEC_APPROVAL",
|
|
1312
|
+
description,
|
|
1313
|
+
roomId: request.roomId,
|
|
1314
|
+
options: EXEC_APPROVAL_OPTIONS,
|
|
1315
|
+
timeoutMs: request.timeoutMs ?? EXEC_APPROVAL_DEFAULTS.timeoutMs,
|
|
1316
|
+
timeoutDefault: "deny",
|
|
1317
|
+
tags: ["EXEC", request.id],
|
|
1318
|
+
metadata: {
|
|
1319
|
+
execRequest: {
|
|
1320
|
+
id: request.id,
|
|
1321
|
+
command: request.command,
|
|
1322
|
+
cwd: request.cwd,
|
|
1323
|
+
security: request.security,
|
|
1324
|
+
ask: request.ask,
|
|
1325
|
+
agentId: request.agentId,
|
|
1326
|
+
resolvedPath: request.resolvedPath
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
const decision = mapOptionToDecision(result.selectedOption);
|
|
1331
|
+
if (decision === "allow-always" && request.resolvedPath) {
|
|
1332
|
+
await this.addToAllowlist(request.resolvedPath, request.agentId);
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
decision,
|
|
1336
|
+
timedOut: result.timedOut,
|
|
1337
|
+
resolvedBy: result.resolvedBy
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
async requestApprovalAsync(request, callbacks) {
|
|
1341
|
+
const approvalService = this.runtime.getService("approval");
|
|
1342
|
+
if (!approvalService) {
|
|
1343
|
+
logger2.warn({ src: "service:exec_approval" }, "ApprovalService not available");
|
|
1344
|
+
if (callbacks?.onDenied) {
|
|
1345
|
+
await callbacks.onDenied();
|
|
1346
|
+
}
|
|
1347
|
+
throw new Error("ApprovalService not available");
|
|
1348
|
+
}
|
|
1349
|
+
const descriptionLines = ["**Exec Approval Required**", "", `Command: \`${request.command}\``];
|
|
1350
|
+
if (request.cwd) {
|
|
1351
|
+
descriptionLines.push(`CWD: \`${request.cwd}\``);
|
|
1352
|
+
}
|
|
1353
|
+
const taskId = await approvalService.requestApprovalAsync({
|
|
1354
|
+
name: "EXEC_APPROVAL",
|
|
1355
|
+
description: descriptionLines.join(`
|
|
1356
|
+
`),
|
|
1357
|
+
roomId: request.roomId,
|
|
1358
|
+
options: EXEC_APPROVAL_OPTIONS,
|
|
1359
|
+
timeoutMs: request.timeoutMs ?? EXEC_APPROVAL_DEFAULTS.timeoutMs,
|
|
1360
|
+
timeoutDefault: "deny",
|
|
1361
|
+
tags: ["EXEC", request.id],
|
|
1362
|
+
metadata: {
|
|
1363
|
+
execRequest: {
|
|
1364
|
+
id: request.id,
|
|
1365
|
+
command: request.command,
|
|
1366
|
+
cwd: request.cwd,
|
|
1367
|
+
security: request.security,
|
|
1368
|
+
ask: request.ask,
|
|
1369
|
+
agentId: request.agentId,
|
|
1370
|
+
resolvedPath: request.resolvedPath
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
onSelect: async (option, _task, _rt) => {
|
|
1374
|
+
const decision = mapOptionToDecision(option);
|
|
1375
|
+
if (decision === "allow-always" && request.resolvedPath) {
|
|
1376
|
+
await this.addToAllowlist(request.resolvedPath, request.agentId);
|
|
1377
|
+
}
|
|
1378
|
+
if (decision === "allow-once" || decision === "allow-always") {
|
|
1379
|
+
if (callbacks?.onApproved) {
|
|
1380
|
+
await callbacks.onApproved(decision);
|
|
1381
|
+
}
|
|
1382
|
+
} else {
|
|
1383
|
+
if (callbacks?.onDenied) {
|
|
1384
|
+
await callbacks.onDenied();
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
onTimeout: async (_task, _rt) => {
|
|
1389
|
+
if (callbacks?.onTimeout) {
|
|
1390
|
+
await callbacks.onTimeout();
|
|
1391
|
+
} else if (callbacks?.onDenied) {
|
|
1392
|
+
await callbacks.onDenied();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
return taskId;
|
|
1397
|
+
}
|
|
1398
|
+
async addToAllowlist(pattern, agentId) {
|
|
1399
|
+
const approvals = loadApprovals();
|
|
1400
|
+
const added = addAllowlistEntry(approvals, agentId ?? this.runtime.agentId, pattern);
|
|
1401
|
+
if (added) {
|
|
1402
|
+
this.approvalConfig = null;
|
|
1403
|
+
}
|
|
1404
|
+
return added;
|
|
1405
|
+
}
|
|
1406
|
+
async cancelApproval(taskId) {
|
|
1407
|
+
const approvalService = this.runtime.getService("approval");
|
|
1408
|
+
if (approvalService) {
|
|
1409
|
+
await approvalService.cancelApproval(taskId);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async getPendingApprovals(roomId) {
|
|
1413
|
+
if (!this.runtime) {
|
|
1414
|
+
logger2.warn({ src: "service:exec_approval" }, "Cannot get pending approvals - runtime not available");
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
const tasks = await this.runtime.getTasks({
|
|
1419
|
+
roomId,
|
|
1420
|
+
tags: ["AWAITING_CHOICE", "EXEC"],
|
|
1421
|
+
agentIds: [this.runtime.agentId]
|
|
1422
|
+
});
|
|
1423
|
+
if (!tasks)
|
|
1424
|
+
return [];
|
|
1425
|
+
return tasks.filter((t) => t.metadata?.execRequest).map((t) => {
|
|
1426
|
+
const execRequest = t.metadata?.execRequest;
|
|
1427
|
+
return {
|
|
1428
|
+
id: execRequest.id,
|
|
1429
|
+
command: execRequest.command,
|
|
1430
|
+
cwd: execRequest.cwd,
|
|
1431
|
+
security: execRequest.security,
|
|
1432
|
+
ask: execRequest.ask,
|
|
1433
|
+
agentId: execRequest.agentId,
|
|
1434
|
+
resolvedPath: execRequest.resolvedPath,
|
|
1435
|
+
roomId
|
|
1436
|
+
};
|
|
1437
|
+
});
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
logger2.error({ src: "service:exec_approval", error, roomId }, "Failed to get pending approvals");
|
|
1440
|
+
return [];
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function mapOptionToDecision(option) {
|
|
1445
|
+
switch (option) {
|
|
1446
|
+
case "allow-once":
|
|
1447
|
+
return "allow-once";
|
|
1448
|
+
case "allow-always":
|
|
1449
|
+
return "allow-always";
|
|
1450
|
+
default:
|
|
1451
|
+
return "deny";
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
// providers/shellHistoryProvider.ts
|
|
1455
|
+
import {
|
|
1456
|
+
addHeader,
|
|
1457
|
+
logger as logger3
|
|
1458
|
+
} from "@elizaos/core";
|
|
1459
|
+
|
|
1460
|
+
// generated/specs/specs.ts
|
|
1461
|
+
var coreActionsSpec = {
|
|
1462
|
+
version: "1.0.0",
|
|
1463
|
+
actions: []
|
|
1464
|
+
};
|
|
1465
|
+
var allActionsSpec = {
|
|
1466
|
+
version: "1.0.0",
|
|
1467
|
+
actions: []
|
|
1468
|
+
};
|
|
1469
|
+
var coreProvidersSpec = {
|
|
1470
|
+
version: "1.0.0",
|
|
1471
|
+
providers: [
|
|
1472
|
+
{
|
|
1473
|
+
name: "SHELL_HISTORY",
|
|
1474
|
+
description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
|
|
1475
|
+
dynamic: true
|
|
1476
|
+
}
|
|
1477
|
+
]
|
|
1478
|
+
};
|
|
1479
|
+
var allProvidersSpec = {
|
|
1480
|
+
version: "1.0.0",
|
|
1481
|
+
providers: [
|
|
1482
|
+
{
|
|
1483
|
+
name: "SHELL_HISTORY",
|
|
1484
|
+
description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
|
|
1485
|
+
dynamic: true
|
|
1486
|
+
}
|
|
1487
|
+
]
|
|
1488
|
+
};
|
|
1489
|
+
var coreActionDocs = coreActionsSpec.actions;
|
|
1490
|
+
var allActionDocs = allActionsSpec.actions;
|
|
1491
|
+
var coreProviderDocs = coreProvidersSpec.providers;
|
|
1492
|
+
var allProviderDocs = allProvidersSpec.providers;
|
|
1493
|
+
|
|
1494
|
+
// generated/specs/spec-helpers.ts
|
|
1495
|
+
var coreActionMap = new Map(coreActionDocs.map((doc) => [doc.name, doc]));
|
|
1496
|
+
var allActionMap = new Map(allActionDocs.map((doc) => [doc.name, doc]));
|
|
1497
|
+
var coreProviderMap = new Map(coreProviderDocs.map((doc) => [doc.name, doc]));
|
|
1498
|
+
var allProviderMap = new Map(allProviderDocs.map((doc) => [doc.name, doc]));
|
|
1499
|
+
function getProviderSpec(name) {
|
|
1500
|
+
return coreProviderMap.get(name) ?? allProviderMap.get(name);
|
|
1501
|
+
}
|
|
1502
|
+
function requireProviderSpec(name) {
|
|
1503
|
+
const spec = getProviderSpec(name);
|
|
1504
|
+
if (!spec) {
|
|
1505
|
+
throw new Error(`Provider spec not found: ${name}`);
|
|
1506
|
+
}
|
|
1507
|
+
return spec;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// providers/shellHistoryProvider.ts
|
|
1511
|
+
var MAX_OUTPUT_LENGTH = 8000;
|
|
1512
|
+
var TRUNCATE_SEGMENT_LENGTH = 4000;
|
|
1513
|
+
var spec = requireProviderSpec("SHELL_HISTORY");
|
|
1514
|
+
var shellHistoryProvider = {
|
|
1515
|
+
name: spec.name,
|
|
1516
|
+
description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
|
|
1517
|
+
descriptionCompressed: "Recent shell history, cwd, and file ops in restricted env.",
|
|
1518
|
+
position: 99,
|
|
1519
|
+
contexts: ["terminal", "code"],
|
|
1520
|
+
contextGate: { anyOf: ["terminal", "code"] },
|
|
1521
|
+
cacheStable: false,
|
|
1522
|
+
cacheScope: "turn",
|
|
1523
|
+
dynamic: true,
|
|
1524
|
+
get: async (runtime, message, _state) => {
|
|
1525
|
+
try {
|
|
1526
|
+
const shellService = runtime.getService("shell");
|
|
1527
|
+
if (!shellService) {
|
|
1528
|
+
logger3.warn("[shellHistoryProvider] Shell service not found");
|
|
1529
|
+
return {
|
|
1530
|
+
values: {
|
|
1531
|
+
shellHistory: "Shell service is not available",
|
|
1532
|
+
currentWorkingDirectory: "N/A",
|
|
1533
|
+
allowedDirectory: "N/A"
|
|
1534
|
+
},
|
|
1535
|
+
text: addHeader("# Shell Status", "Shell service is not available"),
|
|
1536
|
+
data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
const conversationId = message.roomId || message.agentId;
|
|
1540
|
+
if (!conversationId) {
|
|
1541
|
+
return {
|
|
1542
|
+
text: "No conversation ID available",
|
|
1543
|
+
values: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" },
|
|
1544
|
+
data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
const history = shellService.getCommandHistory(conversationId, 10);
|
|
1548
|
+
const cwd = shellService.getCurrentDirectory(conversationId);
|
|
1549
|
+
const allowedDir = shellService.getAllowedDirectory();
|
|
1550
|
+
let historyText = "No commands in history.";
|
|
1551
|
+
if (history.length > 0) {
|
|
1552
|
+
historyText = history.map((entry) => {
|
|
1553
|
+
let entryStr = `[${new Date(entry.timestamp).toISOString()}] ${entry.workingDirectory}> ${entry.command}`;
|
|
1554
|
+
if (entry.stdout) {
|
|
1555
|
+
if (entry.stdout.length > MAX_OUTPUT_LENGTH) {
|
|
1556
|
+
entryStr += `
|
|
1557
|
+
Output: ${entry.stdout.substring(0, TRUNCATE_SEGMENT_LENGTH)}
|
|
1558
|
+
... [TRUNCATED] ...
|
|
1559
|
+
${entry.stdout.substring(entry.stdout.length - TRUNCATE_SEGMENT_LENGTH)}`;
|
|
1560
|
+
} else {
|
|
1561
|
+
entryStr += `
|
|
1562
|
+
Output: ${entry.stdout}`;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (entry.stderr) {
|
|
1566
|
+
if (entry.stderr.length > MAX_OUTPUT_LENGTH) {
|
|
1567
|
+
entryStr += `
|
|
1568
|
+
Error: ${entry.stderr.substring(0, TRUNCATE_SEGMENT_LENGTH)}
|
|
1569
|
+
... [TRUNCATED] ...
|
|
1570
|
+
${entry.stderr.substring(entry.stderr.length - TRUNCATE_SEGMENT_LENGTH)}`;
|
|
1571
|
+
} else {
|
|
1572
|
+
entryStr += `
|
|
1573
|
+
Error: ${entry.stderr}`;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
entryStr += `
|
|
1577
|
+
Exit Code: ${entry.exitCode}`;
|
|
1578
|
+
if (entry.fileOperations && entry.fileOperations.length > 0) {
|
|
1579
|
+
entryStr += `
|
|
1580
|
+
File Operations:`;
|
|
1581
|
+
entry.fileOperations.forEach((op) => {
|
|
1582
|
+
if (op.secondaryTarget) {
|
|
1583
|
+
entryStr += `
|
|
1584
|
+
- ${op.type}: ${op.target} → ${op.secondaryTarget}`;
|
|
1585
|
+
} else {
|
|
1586
|
+
entryStr += `
|
|
1587
|
+
- ${op.type}: ${op.target}`;
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
return entryStr;
|
|
1592
|
+
}).join(`
|
|
1593
|
+
|
|
1594
|
+
`);
|
|
1595
|
+
}
|
|
1596
|
+
const recentFileOps = history.filter((entry) => entry.fileOperations && entry.fileOperations.length > 0).flatMap((entry) => entry.fileOperations ?? []).slice(-5);
|
|
1597
|
+
let fileOpsText = "";
|
|
1598
|
+
if (recentFileOps.length > 0) {
|
|
1599
|
+
fileOpsText = `
|
|
1600
|
+
|
|
1601
|
+
` + addHeader("# Recent File Operations", recentFileOps.map((op) => {
|
|
1602
|
+
if (op.secondaryTarget) {
|
|
1603
|
+
return `- ${op.type}: ${op.target} → ${op.secondaryTarget}`;
|
|
1604
|
+
}
|
|
1605
|
+
return `- ${op.type}: ${op.target}`;
|
|
1606
|
+
}).join(`
|
|
1607
|
+
`));
|
|
1608
|
+
}
|
|
1609
|
+
const text = `Current Directory: ${cwd}
|
|
1610
|
+
Allowed Directory: ${allowedDir}
|
|
1611
|
+
|
|
1612
|
+
${addHeader("# Shell History (Last 10)", historyText)}${fileOpsText}`;
|
|
1613
|
+
return {
|
|
1614
|
+
values: {
|
|
1615
|
+
shellHistory: historyText,
|
|
1616
|
+
currentWorkingDirectory: cwd,
|
|
1617
|
+
allowedDirectory: allowedDir
|
|
1618
|
+
},
|
|
1619
|
+
text,
|
|
1620
|
+
data: {
|
|
1621
|
+
historyCount: history.length,
|
|
1622
|
+
cwd,
|
|
1623
|
+
allowedDir
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
} catch {
|
|
1627
|
+
return {
|
|
1628
|
+
values: {
|
|
1629
|
+
shellHistory: "",
|
|
1630
|
+
currentWorkingDirectory: "N/A",
|
|
1631
|
+
allowedDirectory: "N/A"
|
|
1632
|
+
},
|
|
1633
|
+
text: "",
|
|
1634
|
+
data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
// services/shellService.ts
|
|
1640
|
+
import path7 from "node:path";
|
|
1641
|
+
import { logger as logger6, Service as Service2 } from "@elizaos/core";
|
|
1642
|
+
import { isCloudExecutionMode, shouldUseSandboxExecution } from "@elizaos/shared";
|
|
1643
|
+
import spawn2 from "cross-spawn";
|
|
1644
|
+
|
|
1645
|
+
// utils/config.ts
|
|
1646
|
+
import fs3 from "node:fs";
|
|
1647
|
+
import path3 from "node:path";
|
|
1648
|
+
import { logger as logger4 } from "@elizaos/core";
|
|
1649
|
+
import { z } from "zod";
|
|
1650
|
+
var configSchema = z.object({
|
|
1651
|
+
enabled: z.boolean(),
|
|
1652
|
+
allowedDirectory: z.string(),
|
|
1653
|
+
timeout: z.number().positive().default(30000),
|
|
1654
|
+
forbiddenCommands: z.array(z.string()),
|
|
1655
|
+
maxOutputChars: z.number().positive().default(200000),
|
|
1656
|
+
pendingMaxOutputChars: z.number().positive().default(200000),
|
|
1657
|
+
defaultBackgroundMs: z.number().positive().default(1e4),
|
|
1658
|
+
allowBackground: z.boolean().default(true)
|
|
1659
|
+
});
|
|
1660
|
+
var DEFAULT_FORBIDDEN_COMMANDS = [
|
|
1661
|
+
"rm -rf /",
|
|
1662
|
+
"rmdir",
|
|
1663
|
+
"chmod 777",
|
|
1664
|
+
"chown",
|
|
1665
|
+
"chgrp",
|
|
1666
|
+
"shutdown",
|
|
1667
|
+
"reboot",
|
|
1668
|
+
"halt",
|
|
1669
|
+
"poweroff",
|
|
1670
|
+
"kill -9",
|
|
1671
|
+
"killall",
|
|
1672
|
+
"pkill",
|
|
1673
|
+
"sudo rm -rf",
|
|
1674
|
+
"su",
|
|
1675
|
+
"passwd",
|
|
1676
|
+
"useradd",
|
|
1677
|
+
"userdel",
|
|
1678
|
+
"groupadd",
|
|
1679
|
+
"groupdel",
|
|
1680
|
+
"format",
|
|
1681
|
+
"fdisk",
|
|
1682
|
+
"mkfs",
|
|
1683
|
+
"dd if=/dev/zero",
|
|
1684
|
+
"shred",
|
|
1685
|
+
":(){:|:&};:"
|
|
1686
|
+
];
|
|
1687
|
+
function loadShellConfig() {
|
|
1688
|
+
const allowedDirectory = process.env.SHELL_ALLOWED_DIRECTORY || process.cwd();
|
|
1689
|
+
const timeout = parseInt(process.env.SHELL_TIMEOUT || "30000", 10);
|
|
1690
|
+
const maxOutputChars = parseInt(process.env.SHELL_MAX_OUTPUT_CHARS || "200000", 10);
|
|
1691
|
+
const pendingMaxOutputChars = parseInt(process.env.SHELL_PENDING_MAX_OUTPUT_CHARS || "200000", 10);
|
|
1692
|
+
const defaultBackgroundMs = parseInt(process.env.SHELL_BACKGROUND_MS || "10000", 10);
|
|
1693
|
+
const allowBackground = process.env.SHELL_ALLOW_BACKGROUND !== "false";
|
|
1694
|
+
const customForbidden = process.env.SHELL_FORBIDDEN_COMMANDS ? process.env.SHELL_FORBIDDEN_COMMANDS.split(",").map((cmd) => cmd.trim()) : [];
|
|
1695
|
+
const forbiddenCommands = [...new Set([...DEFAULT_FORBIDDEN_COMMANDS, ...customForbidden])];
|
|
1696
|
+
const config = {
|
|
1697
|
+
enabled: true,
|
|
1698
|
+
allowedDirectory,
|
|
1699
|
+
timeout,
|
|
1700
|
+
forbiddenCommands,
|
|
1701
|
+
maxOutputChars,
|
|
1702
|
+
pendingMaxOutputChars,
|
|
1703
|
+
defaultBackgroundMs,
|
|
1704
|
+
allowBackground
|
|
1705
|
+
};
|
|
1706
|
+
const parseResult = configSchema.safeParse(config);
|
|
1707
|
+
if (!parseResult.success) {
|
|
1708
|
+
const errorMessage = parseResult.error.issues[0]?.message || parseResult.error.toString();
|
|
1709
|
+
throw new Error(`Shell plugin configuration error: ${errorMessage}`);
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
const stats = fs3.statSync(allowedDirectory);
|
|
1713
|
+
if (!stats.isDirectory()) {
|
|
1714
|
+
throw new Error(`SHELL_ALLOWED_DIRECTORY is not a directory: ${allowedDirectory}`);
|
|
1715
|
+
}
|
|
1716
|
+
config.allowedDirectory = path3.resolve(allowedDirectory);
|
|
1717
|
+
logger4.info(`Shell plugin enabled with allowed directory: ${config.allowedDirectory}, ` + `background: ${allowBackground}, timeout: ${timeout}ms`);
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1720
|
+
throw new Error(`SHELL_ALLOWED_DIRECTORY does not exist: ${allowedDirectory}`);
|
|
1721
|
+
}
|
|
1722
|
+
throw error;
|
|
1723
|
+
}
|
|
1724
|
+
return config;
|
|
1725
|
+
}
|
|
1726
|
+
// utils/pathUtils.ts
|
|
1727
|
+
import path4 from "node:path";
|
|
1728
|
+
import { logger as logger5 } from "@elizaos/core";
|
|
1729
|
+
function validatePath(commandPath, allowedDir, currentDir) {
|
|
1730
|
+
const resolvedPath = path4.resolve(currentDir, commandPath);
|
|
1731
|
+
const normalizedPath = path4.normalize(resolvedPath);
|
|
1732
|
+
const normalizedAllowed = path4.normalize(allowedDir);
|
|
1733
|
+
const relative = path4.relative(normalizedAllowed, normalizedPath);
|
|
1734
|
+
if (relative.startsWith("..") || path4.isAbsolute(relative)) {
|
|
1735
|
+
logger5.warn(`Path validation failed: ${normalizedPath} is outside allowed directory ${normalizedAllowed}`);
|
|
1736
|
+
return null;
|
|
1737
|
+
}
|
|
1738
|
+
return normalizedPath;
|
|
1739
|
+
}
|
|
1740
|
+
function isSafeCommand(command) {
|
|
1741
|
+
const pathTraversalPatterns = [/\.\.\//g, /\.\.\\/g, /\/\.\./g, /\\\.\./g];
|
|
1742
|
+
const dangerousPatterns = [/\$\(/g, /`[^']*`/g, /\|\s*sudo/g, /;\s*sudo/g, /&\s*&/g, /\|\s*\|/g];
|
|
1743
|
+
for (const pattern of pathTraversalPatterns) {
|
|
1744
|
+
if (pattern.test(command)) {
|
|
1745
|
+
logger5.warn(`Path traversal detected in command: ${command}`);
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
for (const pattern of dangerousPatterns) {
|
|
1750
|
+
if (pattern.test(command)) {
|
|
1751
|
+
logger5.warn(`Dangerous pattern detected in command: ${command}`);
|
|
1752
|
+
return false;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const pipeCount = (command.match(/\|/g) || []).length;
|
|
1756
|
+
if (pipeCount > 1) {
|
|
1757
|
+
logger5.warn(`Multiple pipes detected in command: ${command}`);
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
function extractBaseCommand(fullCommand) {
|
|
1763
|
+
const parts = fullCommand.trim().split(/\s+/);
|
|
1764
|
+
return parts[0] || "";
|
|
1765
|
+
}
|
|
1766
|
+
function isForbiddenCommand(command, forbiddenCommands) {
|
|
1767
|
+
const normalizedCommand = command.trim().toLowerCase();
|
|
1768
|
+
return forbiddenCommands.some((forbidden) => {
|
|
1769
|
+
const forbiddenLower = forbidden.toLowerCase();
|
|
1770
|
+
if (normalizedCommand.startsWith(forbiddenLower)) {
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
if (!forbidden.includes(" ")) {
|
|
1774
|
+
const baseCommand = extractBaseCommand(command);
|
|
1775
|
+
if (baseCommand.toLowerCase() === forbiddenLower) {
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return false;
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
// utils/ptyKeys.ts
|
|
1783
|
+
var ESC = "\x1B";
|
|
1784
|
+
var CR = "\r";
|
|
1785
|
+
var TAB = "\t";
|
|
1786
|
+
var BACKSPACE = "";
|
|
1787
|
+
var BRACKETED_PASTE_START2 = `${ESC}[200~`;
|
|
1788
|
+
var BRACKETED_PASTE_END2 = `${ESC}[201~`;
|
|
1789
|
+
function escapeRegExp(value) {
|
|
1790
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1791
|
+
}
|
|
1792
|
+
var namedKeyMap = new Map([
|
|
1793
|
+
["enter", CR],
|
|
1794
|
+
["return", CR],
|
|
1795
|
+
["tab", TAB],
|
|
1796
|
+
["escape", ESC],
|
|
1797
|
+
["esc", ESC],
|
|
1798
|
+
["space", " "],
|
|
1799
|
+
["bspace", BACKSPACE],
|
|
1800
|
+
["backspace", BACKSPACE],
|
|
1801
|
+
["up", `${ESC}[A`],
|
|
1802
|
+
["down", `${ESC}[B`],
|
|
1803
|
+
["right", `${ESC}[C`],
|
|
1804
|
+
["left", `${ESC}[D`],
|
|
1805
|
+
["home", `${ESC}[1~`],
|
|
1806
|
+
["end", `${ESC}[4~`],
|
|
1807
|
+
["pageup", `${ESC}[5~`],
|
|
1808
|
+
["pgup", `${ESC}[5~`],
|
|
1809
|
+
["ppage", `${ESC}[5~`],
|
|
1810
|
+
["pagedown", `${ESC}[6~`],
|
|
1811
|
+
["pgdn", `${ESC}[6~`],
|
|
1812
|
+
["npage", `${ESC}[6~`],
|
|
1813
|
+
["insert", `${ESC}[2~`],
|
|
1814
|
+
["ic", `${ESC}[2~`],
|
|
1815
|
+
["delete", `${ESC}[3~`],
|
|
1816
|
+
["del", `${ESC}[3~`],
|
|
1817
|
+
["dc", `${ESC}[3~`],
|
|
1818
|
+
["btab", `${ESC}[Z`],
|
|
1819
|
+
["f1", `${ESC}OP`],
|
|
1820
|
+
["f2", `${ESC}OQ`],
|
|
1821
|
+
["f3", `${ESC}OR`],
|
|
1822
|
+
["f4", `${ESC}OS`],
|
|
1823
|
+
["f5", `${ESC}[15~`],
|
|
1824
|
+
["f6", `${ESC}[17~`],
|
|
1825
|
+
["f7", `${ESC}[18~`],
|
|
1826
|
+
["f8", `${ESC}[19~`],
|
|
1827
|
+
["f9", `${ESC}[20~`],
|
|
1828
|
+
["f10", `${ESC}[21~`],
|
|
1829
|
+
["f11", `${ESC}[23~`],
|
|
1830
|
+
["f12", `${ESC}[24~`],
|
|
1831
|
+
["kp/", `${ESC}Oo`],
|
|
1832
|
+
["kp*", `${ESC}Oj`],
|
|
1833
|
+
["kp-", `${ESC}Om`],
|
|
1834
|
+
["kp+", `${ESC}Ok`],
|
|
1835
|
+
["kp7", `${ESC}Ow`],
|
|
1836
|
+
["kp8", `${ESC}Ox`],
|
|
1837
|
+
["kp9", `${ESC}Oy`],
|
|
1838
|
+
["kp4", `${ESC}Ot`],
|
|
1839
|
+
["kp5", `${ESC}Ou`],
|
|
1840
|
+
["kp6", `${ESC}Ov`],
|
|
1841
|
+
["kp1", `${ESC}Oq`],
|
|
1842
|
+
["kp2", `${ESC}Or`],
|
|
1843
|
+
["kp3", `${ESC}Os`],
|
|
1844
|
+
["kp0", `${ESC}Op`],
|
|
1845
|
+
["kp.", `${ESC}On`],
|
|
1846
|
+
["kpenter", `${ESC}OM`]
|
|
1847
|
+
]);
|
|
1848
|
+
var modifiableNamedKeys = new Set([
|
|
1849
|
+
"up",
|
|
1850
|
+
"down",
|
|
1851
|
+
"left",
|
|
1852
|
+
"right",
|
|
1853
|
+
"home",
|
|
1854
|
+
"end",
|
|
1855
|
+
"pageup",
|
|
1856
|
+
"pgup",
|
|
1857
|
+
"ppage",
|
|
1858
|
+
"pagedown",
|
|
1859
|
+
"pgdn",
|
|
1860
|
+
"npage",
|
|
1861
|
+
"insert",
|
|
1862
|
+
"ic",
|
|
1863
|
+
"delete",
|
|
1864
|
+
"del",
|
|
1865
|
+
"dc"
|
|
1866
|
+
]);
|
|
1867
|
+
function encodeKeySequence2(request) {
|
|
1868
|
+
const warnings = [];
|
|
1869
|
+
let data = "";
|
|
1870
|
+
if (request.literal) {
|
|
1871
|
+
data += request.literal;
|
|
1872
|
+
}
|
|
1873
|
+
if (request.hex?.length) {
|
|
1874
|
+
for (const raw of request.hex) {
|
|
1875
|
+
const byte = parseHexByte(raw);
|
|
1876
|
+
if (byte === null) {
|
|
1877
|
+
warnings.push(`Invalid hex byte: ${raw}`);
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
data += String.fromCharCode(byte);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (request.keys?.length) {
|
|
1884
|
+
for (const token of request.keys) {
|
|
1885
|
+
data += encodeKeyToken(token, warnings);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return { data, warnings };
|
|
1889
|
+
}
|
|
1890
|
+
function encodePaste2(text, bracketed = true) {
|
|
1891
|
+
if (!bracketed) {
|
|
1892
|
+
return text;
|
|
1893
|
+
}
|
|
1894
|
+
return `${BRACKETED_PASTE_START2}${text}${BRACKETED_PASTE_END2}`;
|
|
1895
|
+
}
|
|
1896
|
+
function encodeKeyToken(raw, warnings) {
|
|
1897
|
+
const token = raw.trim();
|
|
1898
|
+
if (!token) {
|
|
1899
|
+
return "";
|
|
1900
|
+
}
|
|
1901
|
+
if (token.length === 2 && token.startsWith("^")) {
|
|
1902
|
+
const ctrl = toCtrlChar(token[1]);
|
|
1903
|
+
if (ctrl) {
|
|
1904
|
+
return ctrl;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const parsed = parseModifiers(token);
|
|
1908
|
+
const base = parsed.base;
|
|
1909
|
+
const baseLower = base.toLowerCase();
|
|
1910
|
+
if (baseLower === "tab" && parsed.mods.shift) {
|
|
1911
|
+
return `${ESC}[Z`;
|
|
1912
|
+
}
|
|
1913
|
+
const baseSeq = namedKeyMap.get(baseLower);
|
|
1914
|
+
if (baseSeq) {
|
|
1915
|
+
let seq = baseSeq;
|
|
1916
|
+
if (modifiableNamedKeys.has(baseLower) && hasAnyModifier(parsed.mods)) {
|
|
1917
|
+
const mod = xtermModifier(parsed.mods);
|
|
1918
|
+
if (mod > 1) {
|
|
1919
|
+
const modified = applyXtermModifier(seq, mod);
|
|
1920
|
+
if (modified) {
|
|
1921
|
+
seq = modified;
|
|
1922
|
+
return seq;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (parsed.mods.alt) {
|
|
1927
|
+
return `${ESC}${seq}`;
|
|
1928
|
+
}
|
|
1929
|
+
return seq;
|
|
1930
|
+
}
|
|
1931
|
+
if (base.length === 1) {
|
|
1932
|
+
return applyCharModifiers(base, parsed.mods);
|
|
1933
|
+
}
|
|
1934
|
+
if (parsed.hasModifiers) {
|
|
1935
|
+
warnings.push(`Unknown key "${base}" for modifiers; sending literal.`);
|
|
1936
|
+
}
|
|
1937
|
+
return base;
|
|
1938
|
+
}
|
|
1939
|
+
function parseModifiers(token) {
|
|
1940
|
+
const mods = { ctrl: false, alt: false, shift: false };
|
|
1941
|
+
let rest = token;
|
|
1942
|
+
let sawModifiers = false;
|
|
1943
|
+
while (rest.length > 2 && rest[1] === "-") {
|
|
1944
|
+
const mod = rest[0].toLowerCase();
|
|
1945
|
+
if (mod === "c") {
|
|
1946
|
+
mods.ctrl = true;
|
|
1947
|
+
} else if (mod === "m") {
|
|
1948
|
+
mods.alt = true;
|
|
1949
|
+
} else if (mod === "s") {
|
|
1950
|
+
mods.shift = true;
|
|
1951
|
+
} else {
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
sawModifiers = true;
|
|
1955
|
+
rest = rest.slice(2);
|
|
1956
|
+
}
|
|
1957
|
+
return { mods, base: rest, hasModifiers: sawModifiers };
|
|
1958
|
+
}
|
|
1959
|
+
function applyCharModifiers(char, mods) {
|
|
1960
|
+
let value = char;
|
|
1961
|
+
if (mods.shift && value.length === 1 && /[a-z]/.test(value)) {
|
|
1962
|
+
value = value.toUpperCase();
|
|
1963
|
+
}
|
|
1964
|
+
if (mods.ctrl) {
|
|
1965
|
+
const ctrl = toCtrlChar(value);
|
|
1966
|
+
if (ctrl) {
|
|
1967
|
+
value = ctrl;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (mods.alt) {
|
|
1971
|
+
value = `${ESC}${value}`;
|
|
1972
|
+
}
|
|
1973
|
+
return value;
|
|
1974
|
+
}
|
|
1975
|
+
function toCtrlChar(char) {
|
|
1976
|
+
if (char.length !== 1) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
if (char === "?") {
|
|
1980
|
+
return "";
|
|
1981
|
+
}
|
|
1982
|
+
const code = char.toUpperCase().charCodeAt(0);
|
|
1983
|
+
if (code >= 64 && code <= 95) {
|
|
1984
|
+
return String.fromCharCode(code & 31);
|
|
1985
|
+
}
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
function xtermModifier(mods) {
|
|
1989
|
+
let mod = 1;
|
|
1990
|
+
if (mods.shift) {
|
|
1991
|
+
mod += 1;
|
|
1992
|
+
}
|
|
1993
|
+
if (mods.alt) {
|
|
1994
|
+
mod += 2;
|
|
1995
|
+
}
|
|
1996
|
+
if (mods.ctrl) {
|
|
1997
|
+
mod += 4;
|
|
1998
|
+
}
|
|
1999
|
+
return mod;
|
|
2000
|
+
}
|
|
2001
|
+
function applyXtermModifier(sequence, modifier) {
|
|
2002
|
+
const escPattern = escapeRegExp(ESC);
|
|
2003
|
+
const csiNumber = new RegExp(`^${escPattern}\\[(\\d+)([~A-Z])$`);
|
|
2004
|
+
const csiArrow = new RegExp(`^${escPattern}\\[(A|B|C|D|H|F)$`);
|
|
2005
|
+
const numberMatch = sequence.match(csiNumber);
|
|
2006
|
+
if (numberMatch) {
|
|
2007
|
+
return `${ESC}[${numberMatch[1]};${modifier}${numberMatch[2]}`;
|
|
2008
|
+
}
|
|
2009
|
+
const arrowMatch = sequence.match(csiArrow);
|
|
2010
|
+
if (arrowMatch) {
|
|
2011
|
+
return `${ESC}[1;${modifier}${arrowMatch[1]}`;
|
|
2012
|
+
}
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
function hasAnyModifier(mods) {
|
|
2016
|
+
return mods.ctrl || mods.alt || mods.shift;
|
|
2017
|
+
}
|
|
2018
|
+
function parseHexByte(raw) {
|
|
2019
|
+
const trimmed = raw.trim().toLowerCase();
|
|
2020
|
+
const normalized = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
|
|
2021
|
+
if (!/^[0-9a-f]{1,2}$/.test(normalized)) {
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
const value = Number.parseInt(normalized, 16);
|
|
2025
|
+
if (Number.isNaN(value) || value < 0 || value > 255) {
|
|
2026
|
+
return null;
|
|
2027
|
+
}
|
|
2028
|
+
return value;
|
|
2029
|
+
}
|
|
2030
|
+
var DSR_PATTERN = new RegExp(`${ESC}\\[\\??6n`, "g");
|
|
2031
|
+
function stripDsrRequests2(input) {
|
|
2032
|
+
let requests = 0;
|
|
2033
|
+
const cleaned = input.replace(DSR_PATTERN, () => {
|
|
2034
|
+
requests += 1;
|
|
2035
|
+
return "";
|
|
2036
|
+
});
|
|
2037
|
+
return { cleaned, requests };
|
|
2038
|
+
}
|
|
2039
|
+
function buildCursorPositionResponse2(row = 1, col = 1) {
|
|
2040
|
+
return `\x1B[${row};${col}R`;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// utils/shellUtils.ts
|
|
2044
|
+
import { spawn } from "node:child_process";
|
|
2045
|
+
import { existsSync, statSync } from "node:fs";
|
|
2046
|
+
import { homedir } from "node:os";
|
|
2047
|
+
import path6 from "node:path";
|
|
2048
|
+
|
|
2049
|
+
// utils/terminalCapabilities.ts
|
|
2050
|
+
import fs4 from "node:fs";
|
|
2051
|
+
import path5 from "node:path";
|
|
2052
|
+
var TERMINAL_TOOL_NAMES = [
|
|
2053
|
+
"sh",
|
|
2054
|
+
"git",
|
|
2055
|
+
"rg",
|
|
2056
|
+
"bun",
|
|
2057
|
+
"acpx",
|
|
2058
|
+
"codex",
|
|
2059
|
+
"claude",
|
|
2060
|
+
"opencode"
|
|
2061
|
+
];
|
|
2062
|
+
var ANDROID_PATH_ENTRIES = ["/system/bin", "/system/xbin", "/vendor/bin"];
|
|
2063
|
+
function isAndroidRuntime() {
|
|
2064
|
+
return process.env.ELIZA_PLATFORM?.trim().toLowerCase() === "android" || Boolean(process.env.ANDROID_ROOT || process.env.ANDROID_DATA);
|
|
2065
|
+
}
|
|
2066
|
+
function isIosRuntime() {
|
|
2067
|
+
return process.env.ELIZA_PLATFORM?.trim().toLowerCase() === "ios";
|
|
2068
|
+
}
|
|
2069
|
+
function isStoreBuild() {
|
|
2070
|
+
const variant = process.env.ELIZA_BUILD_VARIANT ?? "";
|
|
2071
|
+
return variant.trim().toLowerCase() === "store";
|
|
2072
|
+
}
|
|
2073
|
+
function runtimeMode() {
|
|
2074
|
+
return (process.env.ELIZA_RUNTIME_MODE ?? process.env.RUNTIME_MODE ?? process.env.LOCAL_RUNTIME_MODE ?? "").trim().toLowerCase();
|
|
2075
|
+
}
|
|
2076
|
+
function executableExists(candidate) {
|
|
2077
|
+
try {
|
|
2078
|
+
fs4.accessSync(candidate, fs4.constants.X_OK);
|
|
2079
|
+
return true;
|
|
2080
|
+
} catch {
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
function pathEntries() {
|
|
2085
|
+
const entries = (process.env.PATH ?? "").split(path5.delimiter).map((entry) => entry.trim()).filter(Boolean);
|
|
2086
|
+
if (isAndroidRuntime()) {
|
|
2087
|
+
for (const entry of ANDROID_PATH_ENTRIES) {
|
|
2088
|
+
if (!entries.includes(entry))
|
|
2089
|
+
entries.push(entry);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return entries;
|
|
2093
|
+
}
|
|
2094
|
+
function resolveExecutable(nameOrPath) {
|
|
2095
|
+
const trimmed = nameOrPath.trim();
|
|
2096
|
+
if (!trimmed)
|
|
2097
|
+
return;
|
|
2098
|
+
if (trimmed.includes("/") || path5.isAbsolute(trimmed)) {
|
|
2099
|
+
return executableExists(trimmed) ? trimmed : undefined;
|
|
2100
|
+
}
|
|
2101
|
+
for (const entry of pathEntries()) {
|
|
2102
|
+
const candidate = path5.join(entry, trimmed);
|
|
2103
|
+
if (executableExists(candidate))
|
|
2104
|
+
return candidate;
|
|
2105
|
+
}
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
function firstExecutable(candidates) {
|
|
2109
|
+
for (const candidate of candidates) {
|
|
2110
|
+
const resolved = resolveExecutable(candidate);
|
|
2111
|
+
if (resolved)
|
|
2112
|
+
return resolved;
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
function resolveTerminalShell() {
|
|
2117
|
+
const explicitEntries = [
|
|
2118
|
+
["CODING_TOOLS_SHELL", process.env.CODING_TOOLS_SHELL],
|
|
2119
|
+
["SHELL", process.env.SHELL]
|
|
2120
|
+
];
|
|
2121
|
+
for (const [key, raw] of explicitEntries) {
|
|
2122
|
+
const value = raw?.trim();
|
|
2123
|
+
if (!value)
|
|
2124
|
+
continue;
|
|
2125
|
+
const resolved = resolveExecutable(value);
|
|
2126
|
+
if (resolved) {
|
|
2127
|
+
return {
|
|
2128
|
+
shell: resolved,
|
|
2129
|
+
args: ["-c"],
|
|
2130
|
+
available: true,
|
|
2131
|
+
source: key === "CODING_TOOLS_SHELL" ? "env:CODING_TOOLS_SHELL" : "env:SHELL"
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
const candidates = isAndroidRuntime() ? ["/system/bin/sh", "sh"] : ["/bin/bash", "bash", "/bin/sh", "sh"];
|
|
2136
|
+
const shell = firstExecutable(candidates);
|
|
2137
|
+
if (shell) {
|
|
2138
|
+
return {
|
|
2139
|
+
shell,
|
|
2140
|
+
args: ["-c"],
|
|
2141
|
+
available: true,
|
|
2142
|
+
source: "candidate"
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
return {
|
|
2146
|
+
shell: isAndroidRuntime() ? "/system/bin/sh" : "sh",
|
|
2147
|
+
args: ["-c"],
|
|
2148
|
+
available: false,
|
|
2149
|
+
source: "fallback",
|
|
2150
|
+
warning: isAndroidRuntime() ? "No executable POSIX shell was detected. Android direct/AOSP local-yolo builds must expose /system/bin/sh or set CODING_TOOLS_SHELL to an executable shell." : "No executable POSIX shell was detected. Set SHELL or CODING_TOOLS_SHELL to an executable shell."
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
function detectTerminalCapabilities() {
|
|
2154
|
+
return TERMINAL_TOOL_NAMES.map((name) => {
|
|
2155
|
+
if (name === "sh") {
|
|
2156
|
+
const shell = resolveTerminalShell();
|
|
2157
|
+
return {
|
|
2158
|
+
name,
|
|
2159
|
+
path: shell.available ? shell.shell : undefined,
|
|
2160
|
+
available: shell.available
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
const resolved = resolveExecutable(name);
|
|
2164
|
+
return {
|
|
2165
|
+
name,
|
|
2166
|
+
path: resolved,
|
|
2167
|
+
available: Boolean(resolved)
|
|
2168
|
+
};
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
function formatTerminalCapabilities(capabilities = detectTerminalCapabilities()) {
|
|
2172
|
+
return capabilities.map((capability) => capability.available ? `${capability.name}=ok(${capability.path})` : `${capability.name}=missing`).join(" ");
|
|
2173
|
+
}
|
|
2174
|
+
function missingToolMessage(tool) {
|
|
2175
|
+
if (tool === "sh") {
|
|
2176
|
+
return resolveTerminalShell().warning ?? "No executable shell was detected.";
|
|
2177
|
+
}
|
|
2178
|
+
const suffix = isAndroidRuntime() ? " On Android direct/AOSP builds, ensure the binary is staged into the agent image and PATH includes /system/bin or the tool's install directory." : " Install it or add it to PATH.";
|
|
2179
|
+
return `${tool} CLI is not available in PATH.${suffix}`;
|
|
2180
|
+
}
|
|
2181
|
+
function missingTerminalToolForCommand(command) {
|
|
2182
|
+
const tokens = command.trim().split(/\s+/).filter(Boolean);
|
|
2183
|
+
let index = 0;
|
|
2184
|
+
while (tokens[index] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) {
|
|
2185
|
+
index += 1;
|
|
2186
|
+
}
|
|
2187
|
+
const first = tokens[index]?.replace(/^["']|["']$/g, "");
|
|
2188
|
+
if (!first)
|
|
2189
|
+
return;
|
|
2190
|
+
const name = path5.basename(first);
|
|
2191
|
+
if (!TERMINAL_TOOL_NAMES.includes(name) || name === "sh") {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
return resolveExecutable(first) ? undefined : name;
|
|
2195
|
+
}
|
|
2196
|
+
function detectTerminalSupport() {
|
|
2197
|
+
if (isStoreBuild()) {
|
|
2198
|
+
return {
|
|
2199
|
+
supported: false,
|
|
2200
|
+
reason: "store_build",
|
|
2201
|
+
message: "Local terminal execution is unavailable in store builds because the OS sandbox blocks spawning local shells and developer CLIs."
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
if (isIosRuntime()) {
|
|
2205
|
+
return {
|
|
2206
|
+
supported: false,
|
|
2207
|
+
reason: "vanilla_mobile",
|
|
2208
|
+
message: "Local terminal execution is unavailable on iOS because the runtime does not expose shell, coding, or orchestrator subprocess capabilities."
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
if (isAndroidRuntime()) {
|
|
2212
|
+
if (runtimeMode() !== "local-yolo") {
|
|
2213
|
+
return {
|
|
2214
|
+
supported: false,
|
|
2215
|
+
reason: "not_local_yolo",
|
|
2216
|
+
message: "Android direct/AOSP terminal execution requires ELIZA_RUNTIME_MODE=local-yolo so commands run in the local agent environment."
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
const shell = resolveTerminalShell();
|
|
2220
|
+
if (!shell.available) {
|
|
2221
|
+
return {
|
|
2222
|
+
supported: false,
|
|
2223
|
+
reason: "missing_shell",
|
|
2224
|
+
message: shell.warning ?? "Android direct/AOSP terminal execution requires an executable shell. Set CODING_TOOLS_SHELL or SHELL to a staged shell binary."
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return { supported: true };
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// utils/shellUtils.ts
|
|
2232
|
+
var CHUNK_LIMIT = 8 * 1024;
|
|
2233
|
+
function resolvePowerShellPath() {
|
|
2234
|
+
const systemRoot = process.env.SystemRoot || process.env.WINDIR;
|
|
2235
|
+
if (systemRoot) {
|
|
2236
|
+
const candidate = path6.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
2237
|
+
if (existsSync(candidate)) {
|
|
2238
|
+
return candidate;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
return "powershell.exe";
|
|
2242
|
+
}
|
|
2243
|
+
function getShellConfig2() {
|
|
2244
|
+
if (process.platform === "win32") {
|
|
2245
|
+
return {
|
|
2246
|
+
shell: resolvePowerShellPath(),
|
|
2247
|
+
args: ["-NoProfile", "-NonInteractive", "-Command"]
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
const envShell = process.env.SHELL?.trim();
|
|
2251
|
+
const shellName = envShell ? path6.basename(envShell) : "";
|
|
2252
|
+
if (shellName === "fish") {
|
|
2253
|
+
const bash = resolveExecutable("bash");
|
|
2254
|
+
if (bash) {
|
|
2255
|
+
return { shell: bash, args: ["-c"] };
|
|
2256
|
+
}
|
|
2257
|
+
const sh = resolveExecutable("sh");
|
|
2258
|
+
if (sh) {
|
|
2259
|
+
return { shell: sh, args: ["-c"] };
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
const resolved = resolveTerminalShell();
|
|
2263
|
+
return { shell: resolved.shell, args: resolved.args };
|
|
2264
|
+
}
|
|
2265
|
+
function sanitizeBinaryOutput2(text) {
|
|
2266
|
+
const scrubbed = text.replace(/[\p{Format}\p{Surrogate}]/gu, "");
|
|
2267
|
+
if (!scrubbed) {
|
|
2268
|
+
return scrubbed;
|
|
2269
|
+
}
|
|
2270
|
+
const chunks = [];
|
|
2271
|
+
for (const char of scrubbed) {
|
|
2272
|
+
const code = char.codePointAt(0);
|
|
2273
|
+
if (code == null) {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (code === 9 || code === 10 || code === 13) {
|
|
2277
|
+
chunks.push(char);
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (code < 32) {
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
chunks.push(char);
|
|
2284
|
+
}
|
|
2285
|
+
return chunks.join("");
|
|
2286
|
+
}
|
|
2287
|
+
function killProcessTree2(pid) {
|
|
2288
|
+
if (process.platform === "win32") {
|
|
2289
|
+
try {
|
|
2290
|
+
spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
2291
|
+
stdio: "ignore",
|
|
2292
|
+
detached: true
|
|
2293
|
+
});
|
|
2294
|
+
} catch {}
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
process.kill(-pid, "SIGKILL");
|
|
2299
|
+
} catch {
|
|
2300
|
+
try {
|
|
2301
|
+
process.kill(pid, "SIGKILL");
|
|
2302
|
+
} catch {}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
function killSession2(session) {
|
|
2306
|
+
const pid = session.pid ?? session.child?.pid;
|
|
2307
|
+
if (pid) {
|
|
2308
|
+
killProcessTree2(pid);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function coerceEnv2(env) {
|
|
2312
|
+
const record = {};
|
|
2313
|
+
if (!env) {
|
|
2314
|
+
return record;
|
|
2315
|
+
}
|
|
2316
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2317
|
+
if (typeof value === "string") {
|
|
2318
|
+
record[key] = value;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return record;
|
|
2322
|
+
}
|
|
2323
|
+
function resolveWorkdir2(workdir, warnings) {
|
|
2324
|
+
const current = safeCwd();
|
|
2325
|
+
const fallback = current ?? homedir();
|
|
2326
|
+
try {
|
|
2327
|
+
const stats = statSync(workdir);
|
|
2328
|
+
if (stats.isDirectory()) {
|
|
2329
|
+
return workdir;
|
|
2330
|
+
}
|
|
2331
|
+
} catch {}
|
|
2332
|
+
warnings.push(`Warning: workdir "${workdir}" is unavailable; using "${fallback}".`);
|
|
2333
|
+
return fallback;
|
|
2334
|
+
}
|
|
2335
|
+
function safeCwd() {
|
|
2336
|
+
try {
|
|
2337
|
+
const cwd = process.cwd();
|
|
2338
|
+
return existsSync(cwd) ? cwd : null;
|
|
2339
|
+
} catch {
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function clampNumber2(value, defaultValue, min, max) {
|
|
2344
|
+
if (value === undefined || Number.isNaN(value)) {
|
|
2345
|
+
return defaultValue;
|
|
2346
|
+
}
|
|
2347
|
+
return Math.min(Math.max(value, min), max);
|
|
2348
|
+
}
|
|
2349
|
+
function readEnvInt2(key) {
|
|
2350
|
+
const raw = process.env[key];
|
|
2351
|
+
if (!raw) {
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2355
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2356
|
+
}
|
|
2357
|
+
function chunkString2(input, limit = CHUNK_LIMIT) {
|
|
2358
|
+
const chunks = [];
|
|
2359
|
+
for (let i = 0;i < input.length; i += limit) {
|
|
2360
|
+
chunks.push(input.slice(i, i + limit));
|
|
2361
|
+
}
|
|
2362
|
+
return chunks;
|
|
2363
|
+
}
|
|
2364
|
+
function sliceUtf16Safe2(str, start, end) {
|
|
2365
|
+
const effectiveEnd = end ?? str.length;
|
|
2366
|
+
if (start < 0) {
|
|
2367
|
+
const adjustedStart = Math.max(0, str.length + start);
|
|
2368
|
+
return str.slice(adjustedStart, effectiveEnd);
|
|
2369
|
+
}
|
|
2370
|
+
return str.slice(start, effectiveEnd);
|
|
2371
|
+
}
|
|
2372
|
+
function truncateMiddle2(str, max) {
|
|
2373
|
+
if (str.length <= max) {
|
|
2374
|
+
return str;
|
|
2375
|
+
}
|
|
2376
|
+
const half = Math.floor((max - 3) / 2);
|
|
2377
|
+
return `${sliceUtf16Safe2(str, 0, half)}...${sliceUtf16Safe2(str, -half)}`;
|
|
2378
|
+
}
|
|
2379
|
+
function sliceLogLines2(text, offset, limit) {
|
|
2380
|
+
if (!text) {
|
|
2381
|
+
return { slice: "", totalLines: 0, totalChars: 0 };
|
|
2382
|
+
}
|
|
2383
|
+
const normalized = text.replace(/\r\n/g, `
|
|
2384
|
+
`);
|
|
2385
|
+
const lines = normalized.split(`
|
|
2386
|
+
`);
|
|
2387
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
2388
|
+
lines.pop();
|
|
2389
|
+
}
|
|
2390
|
+
const totalLines = lines.length;
|
|
2391
|
+
const totalChars = text.length;
|
|
2392
|
+
let start = typeof offset === "number" && Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
2393
|
+
if (limit !== undefined && offset === undefined) {
|
|
2394
|
+
const tailCount = Math.max(0, Math.floor(limit));
|
|
2395
|
+
start = Math.max(totalLines - tailCount, 0);
|
|
2396
|
+
}
|
|
2397
|
+
const end = typeof limit === "number" && Number.isFinite(limit) ? start + Math.max(0, Math.floor(limit)) : undefined;
|
|
2398
|
+
return { slice: lines.slice(start, end).join(`
|
|
2399
|
+
`), totalLines, totalChars };
|
|
2400
|
+
}
|
|
2401
|
+
function deriveSessionName2(command) {
|
|
2402
|
+
const tokens = tokenizeCommand(command);
|
|
2403
|
+
if (tokens.length === 0) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const verb = tokens[0];
|
|
2407
|
+
let target = tokens.slice(1).find((t) => !t.startsWith("-"));
|
|
2408
|
+
if (!target) {
|
|
2409
|
+
target = tokens[1];
|
|
2410
|
+
}
|
|
2411
|
+
if (!target) {
|
|
2412
|
+
return verb;
|
|
2413
|
+
}
|
|
2414
|
+
const cleaned = truncateMiddle2(stripQuotes(target), 48);
|
|
2415
|
+
return `${stripQuotes(verb)} ${cleaned}`;
|
|
2416
|
+
}
|
|
2417
|
+
function tokenizeCommand(command) {
|
|
2418
|
+
const matches = command.match(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/g) ?? [];
|
|
2419
|
+
return matches.map((token) => stripQuotes(token)).filter(Boolean);
|
|
2420
|
+
}
|
|
2421
|
+
function stripQuotes(value) {
|
|
2422
|
+
const trimmed = value.trim();
|
|
2423
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
2424
|
+
return trimmed.slice(1, -1);
|
|
2425
|
+
}
|
|
2426
|
+
return trimmed;
|
|
2427
|
+
}
|
|
2428
|
+
function formatDuration2(ms) {
|
|
2429
|
+
if (ms < 1000) {
|
|
2430
|
+
return `${ms}ms`;
|
|
2431
|
+
}
|
|
2432
|
+
const seconds = Math.floor(ms / 1000);
|
|
2433
|
+
if (seconds < 60) {
|
|
2434
|
+
return `${seconds}s`;
|
|
2435
|
+
}
|
|
2436
|
+
const minutes = Math.floor(seconds / 60);
|
|
2437
|
+
const rem = seconds % 60;
|
|
2438
|
+
return `${minutes}m${rem.toString().padStart(2, "0")}s`;
|
|
2439
|
+
}
|
|
2440
|
+
function pad2(str, width) {
|
|
2441
|
+
if (str.length >= width) {
|
|
2442
|
+
return str;
|
|
2443
|
+
}
|
|
2444
|
+
return str + " ".repeat(width - str.length);
|
|
2445
|
+
}
|
|
2446
|
+
var DEFAULT_RETRY_CODES = ["EBADF"];
|
|
2447
|
+
function formatSpawnError(err) {
|
|
2448
|
+
if (!(err instanceof Error)) {
|
|
2449
|
+
return String(err);
|
|
2450
|
+
}
|
|
2451
|
+
const details = err;
|
|
2452
|
+
const parts = [];
|
|
2453
|
+
const message = err.message.trim();
|
|
2454
|
+
if (message) {
|
|
2455
|
+
parts.push(message);
|
|
2456
|
+
}
|
|
2457
|
+
if (details.code && !message.includes(details.code)) {
|
|
2458
|
+
parts.push(details.code);
|
|
2459
|
+
}
|
|
2460
|
+
if (details.syscall) {
|
|
2461
|
+
parts.push(`syscall=${details.syscall}`);
|
|
2462
|
+
}
|
|
2463
|
+
if (typeof details.errno === "number") {
|
|
2464
|
+
parts.push(`errno=${details.errno}`);
|
|
2465
|
+
}
|
|
2466
|
+
return parts.join(" ");
|
|
2467
|
+
}
|
|
2468
|
+
function shouldRetry(err, codes) {
|
|
2469
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
2470
|
+
return code.length > 0 && codes.includes(code);
|
|
2471
|
+
}
|
|
2472
|
+
async function spawnAndWaitForSpawn(spawnImpl, argv, options) {
|
|
2473
|
+
const child = spawnImpl(argv[0], argv.slice(1), options);
|
|
2474
|
+
return await new Promise((resolve, reject) => {
|
|
2475
|
+
let settled = false;
|
|
2476
|
+
const cleanup = () => {
|
|
2477
|
+
child.removeListener("error", onError);
|
|
2478
|
+
child.removeListener("spawn", onSpawn);
|
|
2479
|
+
};
|
|
2480
|
+
const finishResolve = () => {
|
|
2481
|
+
if (settled) {
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
settled = true;
|
|
2485
|
+
cleanup();
|
|
2486
|
+
resolve(child);
|
|
2487
|
+
};
|
|
2488
|
+
const onError = (err) => {
|
|
2489
|
+
if (settled) {
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
settled = true;
|
|
2493
|
+
cleanup();
|
|
2494
|
+
reject(err);
|
|
2495
|
+
};
|
|
2496
|
+
const onSpawn = () => {
|
|
2497
|
+
finishResolve();
|
|
2498
|
+
};
|
|
2499
|
+
child.once("error", onError);
|
|
2500
|
+
child.once("spawn", onSpawn);
|
|
2501
|
+
process.nextTick(() => {
|
|
2502
|
+
if (typeof child.pid === "number") {
|
|
2503
|
+
finishResolve();
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
async function spawnWithFallback(params) {
|
|
2509
|
+
const spawnImpl = params.spawnImpl ?? spawn;
|
|
2510
|
+
const retryCodes = params.retryCodes ?? DEFAULT_RETRY_CODES;
|
|
2511
|
+
const baseOptions = { ...params.options };
|
|
2512
|
+
const fallbacks = params.fallbacks ?? [];
|
|
2513
|
+
const attempts = [
|
|
2514
|
+
{ options: baseOptions },
|
|
2515
|
+
...fallbacks.map((fallback) => ({
|
|
2516
|
+
label: fallback.label,
|
|
2517
|
+
options: { ...baseOptions, ...fallback.options }
|
|
2518
|
+
}))
|
|
2519
|
+
];
|
|
2520
|
+
let lastError;
|
|
2521
|
+
for (let index = 0;index < attempts.length; index += 1) {
|
|
2522
|
+
const attempt = attempts[index];
|
|
2523
|
+
try {
|
|
2524
|
+
const child = await spawnAndWaitForSpawn(spawnImpl, params.argv, attempt.options);
|
|
2525
|
+
return {
|
|
2526
|
+
child,
|
|
2527
|
+
usedFallback: index > 0,
|
|
2528
|
+
fallbackLabel: attempt.label
|
|
2529
|
+
};
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
lastError = err;
|
|
2532
|
+
const nextFallback = fallbacks[index];
|
|
2533
|
+
if (!nextFallback || !shouldRetry(err, retryCodes)) {
|
|
2534
|
+
throw err;
|
|
2535
|
+
}
|
|
2536
|
+
params.onFallback?.(err, nextFallback);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
throw lastError;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// services/processRegistry.ts
|
|
2543
|
+
var DEFAULT_JOB_TTL_MS = 30 * 60 * 1000;
|
|
2544
|
+
var MIN_JOB_TTL_MS = 60 * 1000;
|
|
2545
|
+
var MAX_JOB_TTL_MS = 3 * 60 * 60 * 1000;
|
|
2546
|
+
var DEFAULT_PENDING_OUTPUT_CHARS = 30000;
|
|
2547
|
+
function clampTtl(value) {
|
|
2548
|
+
if (!value || Number.isNaN(value)) {
|
|
2549
|
+
return DEFAULT_JOB_TTL_MS;
|
|
2550
|
+
}
|
|
2551
|
+
return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS);
|
|
2552
|
+
}
|
|
2553
|
+
var jobTtlMs = clampTtl(Number.parseInt(process.env.SHELL_JOB_TTL_MS ?? "", 10));
|
|
2554
|
+
var runningSessions = new Map;
|
|
2555
|
+
var finishedSessions = new Map;
|
|
2556
|
+
var sweeper = null;
|
|
2557
|
+
function isSessionIdTaken(id) {
|
|
2558
|
+
return runningSessions.has(id) || finishedSessions.has(id);
|
|
2559
|
+
}
|
|
2560
|
+
var SLUG_ADJECTIVES = [
|
|
2561
|
+
"amber",
|
|
2562
|
+
"briny",
|
|
2563
|
+
"brisk",
|
|
2564
|
+
"calm",
|
|
2565
|
+
"clear",
|
|
2566
|
+
"cool",
|
|
2567
|
+
"crisp",
|
|
2568
|
+
"dawn",
|
|
2569
|
+
"delta",
|
|
2570
|
+
"ember",
|
|
2571
|
+
"faint",
|
|
2572
|
+
"fast",
|
|
2573
|
+
"fresh",
|
|
2574
|
+
"gentle",
|
|
2575
|
+
"glow",
|
|
2576
|
+
"good",
|
|
2577
|
+
"grand",
|
|
2578
|
+
"keen",
|
|
2579
|
+
"kind",
|
|
2580
|
+
"lucky",
|
|
2581
|
+
"marine",
|
|
2582
|
+
"mellow",
|
|
2583
|
+
"mild",
|
|
2584
|
+
"neat",
|
|
2585
|
+
"nimble",
|
|
2586
|
+
"nova",
|
|
2587
|
+
"oceanic",
|
|
2588
|
+
"plaid",
|
|
2589
|
+
"quick",
|
|
2590
|
+
"quiet",
|
|
2591
|
+
"rapid",
|
|
2592
|
+
"salty",
|
|
2593
|
+
"sharp",
|
|
2594
|
+
"swift",
|
|
2595
|
+
"tender",
|
|
2596
|
+
"tidal",
|
|
2597
|
+
"tidy",
|
|
2598
|
+
"tide",
|
|
2599
|
+
"vivid",
|
|
2600
|
+
"warm",
|
|
2601
|
+
"wild",
|
|
2602
|
+
"young"
|
|
2603
|
+
];
|
|
2604
|
+
var SLUG_NOUNS = [
|
|
2605
|
+
"atlas",
|
|
2606
|
+
"basil",
|
|
2607
|
+
"bison",
|
|
2608
|
+
"bloom",
|
|
2609
|
+
"breeze",
|
|
2610
|
+
"canyon",
|
|
2611
|
+
"cedar",
|
|
2612
|
+
"claw",
|
|
2613
|
+
"cloud",
|
|
2614
|
+
"comet",
|
|
2615
|
+
"coral",
|
|
2616
|
+
"cove",
|
|
2617
|
+
"crest",
|
|
2618
|
+
"daisy",
|
|
2619
|
+
"dune",
|
|
2620
|
+
"ember",
|
|
2621
|
+
"falcon",
|
|
2622
|
+
"fjord",
|
|
2623
|
+
"forest",
|
|
2624
|
+
"glade",
|
|
2625
|
+
"gulf",
|
|
2626
|
+
"harbor",
|
|
2627
|
+
"haven",
|
|
2628
|
+
"kelp",
|
|
2629
|
+
"lagoon",
|
|
2630
|
+
"meadow",
|
|
2631
|
+
"mist",
|
|
2632
|
+
"nexus",
|
|
2633
|
+
"ocean",
|
|
2634
|
+
"orbit",
|
|
2635
|
+
"otter",
|
|
2636
|
+
"pine",
|
|
2637
|
+
"prairie",
|
|
2638
|
+
"reef",
|
|
2639
|
+
"ridge",
|
|
2640
|
+
"river",
|
|
2641
|
+
"rook",
|
|
2642
|
+
"sable",
|
|
2643
|
+
"sage",
|
|
2644
|
+
"shell",
|
|
2645
|
+
"shoal",
|
|
2646
|
+
"shore",
|
|
2647
|
+
"slug",
|
|
2648
|
+
"summit",
|
|
2649
|
+
"trail",
|
|
2650
|
+
"valley",
|
|
2651
|
+
"wharf",
|
|
2652
|
+
"willow",
|
|
2653
|
+
"zephyr"
|
|
2654
|
+
];
|
|
2655
|
+
function randomChoice(values, fallback) {
|
|
2656
|
+
return values[Math.floor(Math.random() * values.length)] ?? fallback;
|
|
2657
|
+
}
|
|
2658
|
+
function createSlugBase(words = 2) {
|
|
2659
|
+
const parts = [randomChoice(SLUG_ADJECTIVES, "steady"), randomChoice(SLUG_NOUNS, "harbor")];
|
|
2660
|
+
if (words > 2) {
|
|
2661
|
+
parts.push(randomChoice(SLUG_NOUNS, "reef"));
|
|
2662
|
+
}
|
|
2663
|
+
return parts.join("-");
|
|
2664
|
+
}
|
|
2665
|
+
function createSessionSlug(isTaken) {
|
|
2666
|
+
const isIdTaken = isTaken ?? isSessionIdTaken;
|
|
2667
|
+
for (let attempt = 0;attempt < 12; attempt += 1) {
|
|
2668
|
+
const base = createSlugBase(2);
|
|
2669
|
+
if (!isIdTaken(base)) {
|
|
2670
|
+
return base;
|
|
2671
|
+
}
|
|
2672
|
+
for (let i = 2;i <= 12; i += 1) {
|
|
2673
|
+
const candidate = `${base}-${i}`;
|
|
2674
|
+
if (!isIdTaken(candidate)) {
|
|
2675
|
+
return candidate;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
for (let attempt = 0;attempt < 12; attempt += 1) {
|
|
2680
|
+
const base = createSlugBase(3);
|
|
2681
|
+
if (!isIdTaken(base)) {
|
|
2682
|
+
return base;
|
|
2683
|
+
}
|
|
2684
|
+
for (let i = 2;i <= 12; i += 1) {
|
|
2685
|
+
const candidate = `${base}-${i}`;
|
|
2686
|
+
if (!isIdTaken(candidate)) {
|
|
2687
|
+
return candidate;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
const fallback = `${createSlugBase(3)}-${Math.random().toString(36).slice(2, 5)}`;
|
|
2692
|
+
return isIdTaken(fallback) ? `${fallback}-${Date.now().toString(36)}` : fallback;
|
|
2693
|
+
}
|
|
2694
|
+
function addSession(session) {
|
|
2695
|
+
runningSessions.set(session.id, session);
|
|
2696
|
+
startSweeper();
|
|
2697
|
+
}
|
|
2698
|
+
function getSession(id) {
|
|
2699
|
+
return runningSessions.get(id);
|
|
2700
|
+
}
|
|
2701
|
+
function getFinishedSession(id) {
|
|
2702
|
+
return finishedSessions.get(id);
|
|
2703
|
+
}
|
|
2704
|
+
function deleteSession(id) {
|
|
2705
|
+
runningSessions.delete(id);
|
|
2706
|
+
finishedSessions.delete(id);
|
|
2707
|
+
}
|
|
2708
|
+
function sumPendingChars(buffer) {
|
|
2709
|
+
let total = 0;
|
|
2710
|
+
for (const chunk of buffer) {
|
|
2711
|
+
total += chunk.length;
|
|
2712
|
+
}
|
|
2713
|
+
return total;
|
|
2714
|
+
}
|
|
2715
|
+
function capPendingBuffer(buffer, pendingChars, cap) {
|
|
2716
|
+
if (pendingChars <= cap) {
|
|
2717
|
+
return pendingChars;
|
|
2718
|
+
}
|
|
2719
|
+
const last = buffer.at(-1);
|
|
2720
|
+
if (last && last.length >= cap) {
|
|
2721
|
+
buffer.length = 0;
|
|
2722
|
+
buffer.push(last.slice(last.length - cap));
|
|
2723
|
+
return cap;
|
|
2724
|
+
}
|
|
2725
|
+
while (buffer.length && pendingChars - buffer[0].length >= cap) {
|
|
2726
|
+
pendingChars -= buffer[0].length;
|
|
2727
|
+
buffer.shift();
|
|
2728
|
+
}
|
|
2729
|
+
if (buffer.length && pendingChars > cap) {
|
|
2730
|
+
const overflow = pendingChars - cap;
|
|
2731
|
+
buffer[0] = buffer[0].slice(overflow);
|
|
2732
|
+
pendingChars = cap;
|
|
2733
|
+
}
|
|
2734
|
+
return pendingChars;
|
|
2735
|
+
}
|
|
2736
|
+
function tail(text, max = 2000) {
|
|
2737
|
+
if (text.length <= max) {
|
|
2738
|
+
return text;
|
|
2739
|
+
}
|
|
2740
|
+
return text.slice(text.length - max);
|
|
2741
|
+
}
|
|
2742
|
+
function trimWithCap(text, max) {
|
|
2743
|
+
if (text.length <= max) {
|
|
2744
|
+
return text;
|
|
2745
|
+
}
|
|
2746
|
+
return text.slice(text.length - max);
|
|
2747
|
+
}
|
|
2748
|
+
function appendOutput(session, stream, chunk) {
|
|
2749
|
+
session.pendingStdout ??= [];
|
|
2750
|
+
session.pendingStderr ??= [];
|
|
2751
|
+
session.pendingStdoutChars ??= sumPendingChars(session.pendingStdout);
|
|
2752
|
+
session.pendingStderrChars ??= sumPendingChars(session.pendingStderr);
|
|
2753
|
+
const buffer = stream === "stdout" ? session.pendingStdout : session.pendingStderr;
|
|
2754
|
+
const bufferChars = stream === "stdout" ? session.pendingStdoutChars : session.pendingStderrChars;
|
|
2755
|
+
const pendingCap = Math.min(session.pendingMaxOutputChars ?? DEFAULT_PENDING_OUTPUT_CHARS, session.maxOutputChars);
|
|
2756
|
+
buffer.push(chunk);
|
|
2757
|
+
let pendingChars = bufferChars + chunk.length;
|
|
2758
|
+
if (pendingChars > pendingCap) {
|
|
2759
|
+
session.truncated = true;
|
|
2760
|
+
pendingChars = capPendingBuffer(buffer, pendingChars, pendingCap);
|
|
2761
|
+
}
|
|
2762
|
+
if (stream === "stdout") {
|
|
2763
|
+
session.pendingStdoutChars = pendingChars;
|
|
2764
|
+
} else {
|
|
2765
|
+
session.pendingStderrChars = pendingChars;
|
|
2766
|
+
}
|
|
2767
|
+
session.totalOutputChars += chunk.length;
|
|
2768
|
+
const aggregated = trimWithCap(session.aggregated + chunk, session.maxOutputChars);
|
|
2769
|
+
session.truncated = session.truncated || aggregated.length < session.aggregated.length + chunk.length;
|
|
2770
|
+
session.aggregated = aggregated;
|
|
2771
|
+
session.tail = tail(session.aggregated, 2000);
|
|
2772
|
+
}
|
|
2773
|
+
function drainSession(session) {
|
|
2774
|
+
const stdout = session.pendingStdout.join("");
|
|
2775
|
+
const stderr = session.pendingStderr.join("");
|
|
2776
|
+
session.pendingStdout = [];
|
|
2777
|
+
session.pendingStderr = [];
|
|
2778
|
+
session.pendingStdoutChars = 0;
|
|
2779
|
+
session.pendingStderrChars = 0;
|
|
2780
|
+
return { stdout, stderr };
|
|
2781
|
+
}
|
|
2782
|
+
function moveToFinished(session, status) {
|
|
2783
|
+
runningSessions.delete(session.id);
|
|
2784
|
+
if (!session.backgrounded) {
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
finishedSessions.set(session.id, {
|
|
2788
|
+
id: session.id,
|
|
2789
|
+
command: session.command,
|
|
2790
|
+
scopeKey: session.scopeKey,
|
|
2791
|
+
startedAt: session.startedAt,
|
|
2792
|
+
endedAt: Date.now(),
|
|
2793
|
+
cwd: session.cwd,
|
|
2794
|
+
status,
|
|
2795
|
+
exitCode: session.exitCode,
|
|
2796
|
+
exitSignal: session.exitSignal,
|
|
2797
|
+
aggregated: session.aggregated,
|
|
2798
|
+
tail: session.tail,
|
|
2799
|
+
truncated: session.truncated,
|
|
2800
|
+
totalOutputChars: session.totalOutputChars
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
function markExited(session, exitCode, exitSignal, status) {
|
|
2804
|
+
session.exited = true;
|
|
2805
|
+
session.exitCode = exitCode;
|
|
2806
|
+
session.exitSignal = exitSignal;
|
|
2807
|
+
session.tail = tail(session.aggregated, 2000);
|
|
2808
|
+
moveToFinished(session, status);
|
|
2809
|
+
}
|
|
2810
|
+
function markBackgrounded(session) {
|
|
2811
|
+
session.backgrounded = true;
|
|
2812
|
+
}
|
|
2813
|
+
function listRunningSessions() {
|
|
2814
|
+
return Array.from(runningSessions.values()).filter((s) => s.backgrounded);
|
|
2815
|
+
}
|
|
2816
|
+
function listFinishedSessions() {
|
|
2817
|
+
return Array.from(finishedSessions.values());
|
|
2818
|
+
}
|
|
2819
|
+
function clearFinished() {
|
|
2820
|
+
finishedSessions.clear();
|
|
2821
|
+
}
|
|
2822
|
+
function resetProcessRegistryForTests() {
|
|
2823
|
+
runningSessions.clear();
|
|
2824
|
+
finishedSessions.clear();
|
|
2825
|
+
stopSweeper();
|
|
2826
|
+
}
|
|
2827
|
+
function setJobTtlMs(value) {
|
|
2828
|
+
if (value === undefined || Number.isNaN(value)) {
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
jobTtlMs = clampTtl(value);
|
|
2832
|
+
stopSweeper();
|
|
2833
|
+
startSweeper();
|
|
2834
|
+
}
|
|
2835
|
+
function pruneFinishedSessions() {
|
|
2836
|
+
const cutoff = Date.now() - jobTtlMs;
|
|
2837
|
+
for (const [id, session] of finishedSessions.entries()) {
|
|
2838
|
+
if (session.endedAt < cutoff) {
|
|
2839
|
+
finishedSessions.delete(id);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
function startSweeper() {
|
|
2844
|
+
if (sweeper) {
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
sweeper = setInterval(pruneFinishedSessions, Math.max(30000, jobTtlMs / 6));
|
|
2848
|
+
sweeper.unref();
|
|
2849
|
+
}
|
|
2850
|
+
function stopSweeper() {
|
|
2851
|
+
if (!sweeper) {
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
clearInterval(sweeper);
|
|
2855
|
+
sweeper = null;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// services/shellService.ts
|
|
2859
|
+
var DEFAULT_TIMEOUT_SEC = 1800;
|
|
2860
|
+
|
|
2861
|
+
class ShellService extends Service2 {
|
|
2862
|
+
static serviceType = "shell";
|
|
2863
|
+
shellConfig;
|
|
2864
|
+
currentDirectory;
|
|
2865
|
+
commandHistory;
|
|
2866
|
+
maxHistoryPerConversation = 100;
|
|
2867
|
+
scopeKey;
|
|
2868
|
+
constructor(runtime) {
|
|
2869
|
+
super(runtime);
|
|
2870
|
+
this.shellConfig = loadShellConfig();
|
|
2871
|
+
this.currentDirectory = this.shellConfig.allowedDirectory;
|
|
2872
|
+
this.commandHistory = new Map;
|
|
2873
|
+
}
|
|
2874
|
+
static async start(runtime) {
|
|
2875
|
+
const instance = new ShellService(runtime);
|
|
2876
|
+
logger6.info("Shell service initialized with PTY, background execution, and history tracking");
|
|
2877
|
+
return instance;
|
|
2878
|
+
}
|
|
2879
|
+
async stop() {
|
|
2880
|
+
const runningSessions2 = listRunningSessions();
|
|
2881
|
+
for (const session of runningSessions2) {
|
|
2882
|
+
try {
|
|
2883
|
+
killSession2(session);
|
|
2884
|
+
logger6.debug(`Killed shell session: ${session.id}`);
|
|
2885
|
+
} catch (err) {
|
|
2886
|
+
logger6.warn(`Failed to kill shell session ${session.id}: ${err}`);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
this.commandHistory.clear();
|
|
2890
|
+
logger6.info(`Shell service stopped, cleaned up ${runningSessions2.length} running sessions`);
|
|
2891
|
+
}
|
|
2892
|
+
get capabilityDescription() {
|
|
2893
|
+
return "Execute shell commands with PTY support, background execution, and session management";
|
|
2894
|
+
}
|
|
2895
|
+
getSandboxManager() {
|
|
2896
|
+
const candidate = this.runtime.getSandboxManager?.();
|
|
2897
|
+
return candidate ?? null;
|
|
2898
|
+
}
|
|
2899
|
+
toSandboxWorkdir(workdir) {
|
|
2900
|
+
const relative = path7.relative(process.cwd(), path7.resolve(workdir));
|
|
2901
|
+
if (relative === "")
|
|
2902
|
+
return "/workspace";
|
|
2903
|
+
if (!relative.startsWith("..") && !path7.isAbsolute(relative)) {
|
|
2904
|
+
return `/workspace/${relative}`;
|
|
2905
|
+
}
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
async runSandboxCommand(command, workdir, timeoutMs, env) {
|
|
2909
|
+
const sandboxManager = this.getSandboxManager();
|
|
2910
|
+
if (!sandboxManager) {
|
|
2911
|
+
logger6.error("[shell:sandbox] local-safe denied: SandboxManager unavailable");
|
|
2912
|
+
return {
|
|
2913
|
+
success: false,
|
|
2914
|
+
stdout: "",
|
|
2915
|
+
stderr: "local-safe mode requires SandboxManager, but no sandbox manager is available for command execution.",
|
|
2916
|
+
exitCode: 1,
|
|
2917
|
+
error: "Sandbox unavailable",
|
|
2918
|
+
executedIn: workdir
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
const sandboxWorkdir = this.toSandboxWorkdir(workdir);
|
|
2922
|
+
if (!sandboxWorkdir) {
|
|
2923
|
+
return {
|
|
2924
|
+
success: false,
|
|
2925
|
+
stdout: "",
|
|
2926
|
+
stderr: `local-safe mode can only execute inside the sandbox workspace; cwd is outside process workspace: ${workdir}`,
|
|
2927
|
+
exitCode: 1,
|
|
2928
|
+
error: "Sandbox unavailable",
|
|
2929
|
+
executedIn: workdir
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
logger6.info(`[shell:sandbox] routing exec via SandboxManager: ${command.substring(0, 100)}`);
|
|
2933
|
+
const result = await sandboxManager.exec({
|
|
2934
|
+
command,
|
|
2935
|
+
workdir: sandboxWorkdir,
|
|
2936
|
+
timeoutMs,
|
|
2937
|
+
env
|
|
2938
|
+
});
|
|
2939
|
+
logger6.info(`[shell:sandbox] exec completed: exit=${result.exitCode} duration=${result.durationMs}ms executedInSandbox=${result.executedInSandbox}`);
|
|
2940
|
+
return {
|
|
2941
|
+
success: result.exitCode === 0,
|
|
2942
|
+
stdout: result.stdout,
|
|
2943
|
+
stderr: result.stderr,
|
|
2944
|
+
exitCode: result.exitCode,
|
|
2945
|
+
executedIn: workdir
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
localTerminalUnsupportedMessage() {
|
|
2949
|
+
const support = detectTerminalSupport();
|
|
2950
|
+
return support.supported ? null : support.message ?? "Local terminal execution is unavailable.";
|
|
2951
|
+
}
|
|
2952
|
+
setScopeKey(scopeKey) {
|
|
2953
|
+
this.scopeKey = scopeKey;
|
|
2954
|
+
}
|
|
2955
|
+
async executeCommand(command, conversationId) {
|
|
2956
|
+
if (!command || typeof command !== "string") {
|
|
2957
|
+
return {
|
|
2958
|
+
success: false,
|
|
2959
|
+
stdout: "",
|
|
2960
|
+
stderr: "Invalid command",
|
|
2961
|
+
exitCode: 1,
|
|
2962
|
+
error: "Command must be a non-empty string",
|
|
2963
|
+
executedIn: this.currentDirectory
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
if (isCloudExecutionMode(this.runtime)) {
|
|
2967
|
+
logger6.error("[shell:cloud] local exec disabled in cloud mode");
|
|
2968
|
+
return {
|
|
2969
|
+
success: false,
|
|
2970
|
+
stdout: "",
|
|
2971
|
+
stderr: "Local shell execution disabled in cloud mode.",
|
|
2972
|
+
exitCode: 1,
|
|
2973
|
+
error: "Local shell execution disabled in cloud mode.",
|
|
2974
|
+
executedIn: this.currentDirectory
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
const unsupported = this.localTerminalUnsupportedMessage();
|
|
2978
|
+
if (unsupported) {
|
|
2979
|
+
logger6.error(`[shell:unsupported] ${unsupported}`);
|
|
2980
|
+
return {
|
|
2981
|
+
success: false,
|
|
2982
|
+
stdout: "",
|
|
2983
|
+
stderr: unsupported,
|
|
2984
|
+
exitCode: 1,
|
|
2985
|
+
error: unsupported,
|
|
2986
|
+
executedIn: this.currentDirectory
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
if (!shouldUseSandboxExecution(this.runtime) && this.runtime && "sandboxMode" in this.runtime && this.runtime.sandboxMode) {
|
|
2990
|
+
const hostApiUrl = this.runtime.getSetting("SANDBOX_HOST_API_URL") ?? "http://localhost:2138";
|
|
2991
|
+
const runtimeFetch = this.runtime.fetch ?? globalThis.fetch;
|
|
2992
|
+
logger6.info(`[shell:sandbox] routing exec to ${hostApiUrl}: ${command.substring(0, 100)}`);
|
|
2993
|
+
try {
|
|
2994
|
+
const response = await runtimeFetch(`${hostApiUrl}/api/sandbox/exec`, {
|
|
2995
|
+
method: "POST",
|
|
2996
|
+
headers: { "Content-Type": "application/json" },
|
|
2997
|
+
body: JSON.stringify({
|
|
2998
|
+
command,
|
|
2999
|
+
workdir: this.currentDirectory,
|
|
3000
|
+
timeoutMs: 30000
|
|
3001
|
+
})
|
|
3002
|
+
});
|
|
3003
|
+
const result2 = await response.json();
|
|
3004
|
+
logger6.info(`[shell:sandbox] exec completed: exit=${result2.exitCode} duration=${result2.durationMs}ms`);
|
|
3005
|
+
return {
|
|
3006
|
+
success: result2.exitCode === 0,
|
|
3007
|
+
stdout: result2.stdout,
|
|
3008
|
+
stderr: result2.stderr,
|
|
3009
|
+
exitCode: result2.exitCode,
|
|
3010
|
+
executedIn: this.currentDirectory
|
|
3011
|
+
};
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3014
|
+
logger6.error(`[shell:sandbox] exec failed: ${errMsg}`);
|
|
3015
|
+
return {
|
|
3016
|
+
success: false,
|
|
3017
|
+
stdout: "",
|
|
3018
|
+
stderr: `Sandbox exec failed: ${errMsg}`,
|
|
3019
|
+
exitCode: 1,
|
|
3020
|
+
error: "Sandbox remote execution failed",
|
|
3021
|
+
executedIn: this.currentDirectory
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
const trimmedCommand = command.trim();
|
|
3026
|
+
const missingTool = missingTerminalToolForCommand(trimmedCommand);
|
|
3027
|
+
if (missingTool) {
|
|
3028
|
+
const message = missingToolMessage(missingTool);
|
|
3029
|
+
return {
|
|
3030
|
+
success: false,
|
|
3031
|
+
stdout: "",
|
|
3032
|
+
stderr: message,
|
|
3033
|
+
exitCode: 1,
|
|
3034
|
+
error: message,
|
|
3035
|
+
executedIn: this.currentDirectory
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
if (!isSafeCommand(trimmedCommand)) {
|
|
3039
|
+
return {
|
|
3040
|
+
success: false,
|
|
3041
|
+
stdout: "",
|
|
3042
|
+
stderr: "Command contains forbidden patterns",
|
|
3043
|
+
exitCode: 1,
|
|
3044
|
+
error: "Security policy violation",
|
|
3045
|
+
executedIn: this.currentDirectory
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
if (isForbiddenCommand(trimmedCommand, this.shellConfig.forbiddenCommands)) {
|
|
3049
|
+
return {
|
|
3050
|
+
success: false,
|
|
3051
|
+
stdout: "",
|
|
3052
|
+
stderr: "Command is forbidden by security policy",
|
|
3053
|
+
exitCode: 1,
|
|
3054
|
+
error: "Forbidden command",
|
|
3055
|
+
executedIn: this.currentDirectory
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
if (trimmedCommand.startsWith("cd ")) {
|
|
3059
|
+
const result2 = await this.handleCdCommand(trimmedCommand);
|
|
3060
|
+
this.addToHistory(conversationId, trimmedCommand, result2);
|
|
3061
|
+
return result2;
|
|
3062
|
+
}
|
|
3063
|
+
const result = shouldUseSandboxExecution(this.runtime) ? await this.runSandboxCommand(trimmedCommand, this.currentDirectory, 30000) : await this.runCommandSimple(trimmedCommand);
|
|
3064
|
+
if (result.success) {
|
|
3065
|
+
const fileOps = this.detectFileOperations(trimmedCommand, this.currentDirectory);
|
|
3066
|
+
if (fileOps && conversationId) {
|
|
3067
|
+
this.addToHistory(conversationId, trimmedCommand, result, fileOps);
|
|
3068
|
+
} else {
|
|
3069
|
+
this.addToHistory(conversationId, trimmedCommand, result);
|
|
3070
|
+
}
|
|
3071
|
+
} else {
|
|
3072
|
+
this.addToHistory(conversationId, trimmedCommand, result);
|
|
3073
|
+
}
|
|
3074
|
+
return result;
|
|
3075
|
+
}
|
|
3076
|
+
async exec(command, options = {}) {
|
|
3077
|
+
if (!command || typeof command !== "string") {
|
|
3078
|
+
return {
|
|
3079
|
+
status: "failed",
|
|
3080
|
+
exitCode: 1,
|
|
3081
|
+
durationMs: 0,
|
|
3082
|
+
aggregated: "Invalid command",
|
|
3083
|
+
reason: "Command must be a non-empty string"
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
if (isCloudExecutionMode(this.runtime)) {
|
|
3087
|
+
logger6.error("[shell:cloud] local exec disabled in cloud mode");
|
|
3088
|
+
return {
|
|
3089
|
+
status: "failed",
|
|
3090
|
+
exitCode: 1,
|
|
3091
|
+
durationMs: 0,
|
|
3092
|
+
aggregated: "",
|
|
3093
|
+
reason: "Local shell execution disabled in cloud mode."
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
const unsupported = this.localTerminalUnsupportedMessage();
|
|
3097
|
+
if (unsupported) {
|
|
3098
|
+
logger6.error(`[shell:unsupported] ${unsupported}`);
|
|
3099
|
+
return {
|
|
3100
|
+
status: "failed",
|
|
3101
|
+
exitCode: 1,
|
|
3102
|
+
durationMs: 0,
|
|
3103
|
+
aggregated: "",
|
|
3104
|
+
reason: unsupported
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
const trimmedCommand = command.trim();
|
|
3108
|
+
const missingTool = missingTerminalToolForCommand(trimmedCommand);
|
|
3109
|
+
if (missingTool) {
|
|
3110
|
+
const message = missingToolMessage(missingTool);
|
|
3111
|
+
return {
|
|
3112
|
+
status: "failed",
|
|
3113
|
+
exitCode: 1,
|
|
3114
|
+
durationMs: 0,
|
|
3115
|
+
aggregated: "",
|
|
3116
|
+
reason: message
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
if (!isSafeCommand(trimmedCommand)) {
|
|
3120
|
+
return {
|
|
3121
|
+
status: "failed",
|
|
3122
|
+
exitCode: 1,
|
|
3123
|
+
durationMs: 0,
|
|
3124
|
+
aggregated: "Command contains forbidden patterns",
|
|
3125
|
+
reason: "Security policy violation"
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
if (isForbiddenCommand(trimmedCommand, this.shellConfig.forbiddenCommands)) {
|
|
3129
|
+
return {
|
|
3130
|
+
status: "failed",
|
|
3131
|
+
exitCode: 1,
|
|
3132
|
+
durationMs: 0,
|
|
3133
|
+
aggregated: "Command is forbidden by security policy",
|
|
3134
|
+
reason: "Forbidden command"
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
const warnings = [];
|
|
3138
|
+
const maxOutput = this.shellConfig.maxOutputChars;
|
|
3139
|
+
const pendingMaxOutput = this.shellConfig.pendingMaxOutputChars;
|
|
3140
|
+
const defaultBackgroundMs = this.shellConfig.defaultBackgroundMs;
|
|
3141
|
+
const allowBackground = this.shellConfig.allowBackground;
|
|
3142
|
+
const backgroundRequested = options.background === true;
|
|
3143
|
+
const yieldRequested = typeof options.yieldMs === "number";
|
|
3144
|
+
if (!allowBackground && (backgroundRequested || yieldRequested)) {
|
|
3145
|
+
warnings.push("Warning: background execution is disabled; running synchronously.");
|
|
3146
|
+
}
|
|
3147
|
+
const yieldWindow = allowBackground ? backgroundRequested ? 0 : clampNumber2(options.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120000) : null;
|
|
3148
|
+
const rawWorkdir = options.workdir?.trim() || this.currentDirectory || process.cwd();
|
|
3149
|
+
const resolvedWorkdir = resolveWorkdir2(rawWorkdir, warnings);
|
|
3150
|
+
const validatedWorkdir = validatePath(resolvedWorkdir, this.shellConfig.allowedDirectory, this.currentDirectory);
|
|
3151
|
+
if (!validatedWorkdir) {
|
|
3152
|
+
return {
|
|
3153
|
+
status: "failed",
|
|
3154
|
+
exitCode: 1,
|
|
3155
|
+
durationMs: 0,
|
|
3156
|
+
aggregated: "",
|
|
3157
|
+
reason: `workdir is outside allowed directory: ${resolvedWorkdir}`
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
const workdir = validatedWorkdir;
|
|
3161
|
+
const baseEnv = coerceEnv2(process.env);
|
|
3162
|
+
const mergedEnv = options.env ? { ...baseEnv, ...options.env } : baseEnv;
|
|
3163
|
+
const timeoutSec = typeof options.timeout === "number" && options.timeout > 0 ? options.timeout : DEFAULT_TIMEOUT_SEC;
|
|
3164
|
+
const usePty = options.pty === true;
|
|
3165
|
+
const notifyOnExit = options.notifyOnExit !== false;
|
|
3166
|
+
if (shouldUseSandboxExecution(this.runtime)) {
|
|
3167
|
+
if (backgroundRequested || yieldRequested || usePty) {
|
|
3168
|
+
warnings.push("Warning: local-safe sandbox execution runs synchronously; background, yield, and PTY options are ignored.");
|
|
3169
|
+
}
|
|
3170
|
+
const startedAt = Date.now();
|
|
3171
|
+
const sandboxResult = await this.runSandboxCommand(trimmedCommand, workdir, timeoutSec * 1000, mergedEnv);
|
|
3172
|
+
const warningText2 = warnings.length ? `${warnings.join(`
|
|
3173
|
+
`)}
|
|
3174
|
+
|
|
3175
|
+
` : "";
|
|
3176
|
+
const aggregated = [sandboxResult.stdout, sandboxResult.stderr].filter(Boolean).join(`
|
|
3177
|
+
`);
|
|
3178
|
+
if (!sandboxResult.success) {
|
|
3179
|
+
return {
|
|
3180
|
+
status: "failed",
|
|
3181
|
+
exitCode: sandboxResult.exitCode,
|
|
3182
|
+
durationMs: Date.now() - startedAt,
|
|
3183
|
+
aggregated,
|
|
3184
|
+
cwd: workdir,
|
|
3185
|
+
reason: `${warningText2}${sandboxResult.error ?? sandboxResult.stderr}`
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
return {
|
|
3189
|
+
status: "completed",
|
|
3190
|
+
exitCode: sandboxResult.exitCode,
|
|
3191
|
+
durationMs: Date.now() - startedAt,
|
|
3192
|
+
aggregated: `${warningText2}${aggregated || "(no output)"}`,
|
|
3193
|
+
cwd: workdir
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
const handle = await this.runExecProcess({
|
|
3197
|
+
command: trimmedCommand,
|
|
3198
|
+
workdir,
|
|
3199
|
+
env: mergedEnv,
|
|
3200
|
+
usePty,
|
|
3201
|
+
warnings,
|
|
3202
|
+
maxOutput,
|
|
3203
|
+
pendingMaxOutput,
|
|
3204
|
+
notifyOnExit,
|
|
3205
|
+
scopeKey: options.scopeKey ?? this.scopeKey,
|
|
3206
|
+
sessionKey: options.sessionKey,
|
|
3207
|
+
timeoutSec,
|
|
3208
|
+
onUpdate: options.onUpdate
|
|
3209
|
+
});
|
|
3210
|
+
if (allowBackground && yieldWindow !== null) {
|
|
3211
|
+
if (yieldWindow === 0) {
|
|
3212
|
+
markBackgrounded(handle.session);
|
|
3213
|
+
return {
|
|
3214
|
+
status: "running",
|
|
3215
|
+
sessionId: handle.session.id,
|
|
3216
|
+
pid: handle.session.pid,
|
|
3217
|
+
startedAt: handle.startedAt,
|
|
3218
|
+
cwd: handle.session.cwd,
|
|
3219
|
+
tail: handle.session.tail
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
const raceResult = await Promise.race([
|
|
3223
|
+
handle.promise,
|
|
3224
|
+
new Promise((resolve) => setTimeout(() => resolve("yield"), yieldWindow))
|
|
3225
|
+
]);
|
|
3226
|
+
if (raceResult === "yield" && !handle.session.exited) {
|
|
3227
|
+
markBackgrounded(handle.session);
|
|
3228
|
+
const warningText3 = warnings.length ? `${warnings.join(`
|
|
3229
|
+
`)}
|
|
3230
|
+
|
|
3231
|
+
` : "";
|
|
3232
|
+
return {
|
|
3233
|
+
status: "running",
|
|
3234
|
+
sessionId: handle.session.id,
|
|
3235
|
+
pid: handle.session.pid,
|
|
3236
|
+
startedAt: handle.startedAt,
|
|
3237
|
+
cwd: handle.session.cwd,
|
|
3238
|
+
tail: `${warningText3}Command still running (session ${handle.session.id}, pid ${handle.session.pid ?? "n/a"}).`
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
const outcome2 = raceResult === "yield" ? await handle.promise : raceResult;
|
|
3242
|
+
const warningText2 = warnings.length ? `${warnings.join(`
|
|
3243
|
+
`)}
|
|
3244
|
+
|
|
3245
|
+
` : "";
|
|
3246
|
+
if (outcome2.status === "failed") {
|
|
3247
|
+
return {
|
|
3248
|
+
status: "failed",
|
|
3249
|
+
exitCode: outcome2.exitCode ?? null,
|
|
3250
|
+
durationMs: outcome2.durationMs,
|
|
3251
|
+
aggregated: outcome2.aggregated,
|
|
3252
|
+
cwd: workdir,
|
|
3253
|
+
timedOut: outcome2.timedOut,
|
|
3254
|
+
reason: `${warningText2}${outcome2.reason ?? "Command failed."}`
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
return {
|
|
3258
|
+
status: "completed",
|
|
3259
|
+
exitCode: outcome2.exitCode ?? 0,
|
|
3260
|
+
durationMs: outcome2.durationMs,
|
|
3261
|
+
aggregated: `${warningText2}${outcome2.aggregated || "(no output)"}`,
|
|
3262
|
+
cwd: workdir
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
const outcome = await handle.promise;
|
|
3266
|
+
const warningText = warnings.length ? `${warnings.join(`
|
|
3267
|
+
`)}
|
|
3268
|
+
|
|
3269
|
+
` : "";
|
|
3270
|
+
if (outcome.status === "failed") {
|
|
3271
|
+
return {
|
|
3272
|
+
status: "failed",
|
|
3273
|
+
exitCode: outcome.exitCode ?? null,
|
|
3274
|
+
durationMs: outcome.durationMs,
|
|
3275
|
+
aggregated: outcome.aggregated,
|
|
3276
|
+
cwd: workdir,
|
|
3277
|
+
timedOut: outcome.timedOut,
|
|
3278
|
+
reason: `${warningText}${outcome.reason ?? "Command failed."}`
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
return {
|
|
3282
|
+
status: "completed",
|
|
3283
|
+
exitCode: outcome.exitCode ?? 0,
|
|
3284
|
+
durationMs: outcome.durationMs,
|
|
3285
|
+
aggregated: `${warningText}${outcome.aggregated || "(no output)"}`,
|
|
3286
|
+
cwd: workdir
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
async processAction(params) {
|
|
3290
|
+
const scopeKey = this.scopeKey;
|
|
3291
|
+
const isInScope = (session2) => !scopeKey || session2?.scopeKey === scopeKey;
|
|
3292
|
+
if (params.action === "list") {
|
|
3293
|
+
const running = listRunningSessions().filter((s) => isInScope(s)).map((s) => ({
|
|
3294
|
+
sessionId: s.id,
|
|
3295
|
+
status: "running",
|
|
3296
|
+
pid: s.pid ?? undefined,
|
|
3297
|
+
startedAt: s.startedAt,
|
|
3298
|
+
runtimeMs: Date.now() - s.startedAt,
|
|
3299
|
+
cwd: s.cwd,
|
|
3300
|
+
command: s.command,
|
|
3301
|
+
name: deriveSessionName2(s.command),
|
|
3302
|
+
tail: s.tail,
|
|
3303
|
+
truncated: s.truncated
|
|
3304
|
+
}));
|
|
3305
|
+
const finished2 = listFinishedSessions().filter((s) => isInScope(s)).map((s) => ({
|
|
3306
|
+
sessionId: s.id,
|
|
3307
|
+
status: s.status,
|
|
3308
|
+
startedAt: s.startedAt,
|
|
3309
|
+
endedAt: s.endedAt,
|
|
3310
|
+
runtimeMs: s.endedAt - s.startedAt,
|
|
3311
|
+
cwd: s.cwd,
|
|
3312
|
+
command: s.command,
|
|
3313
|
+
name: deriveSessionName2(s.command),
|
|
3314
|
+
tail: s.tail,
|
|
3315
|
+
truncated: s.truncated,
|
|
3316
|
+
exitCode: s.exitCode ?? undefined,
|
|
3317
|
+
exitSignal: s.exitSignal ?? undefined
|
|
3318
|
+
}));
|
|
3319
|
+
const sessions = [...running, ...finished2].slice().sort((a, b) => b.startedAt - a.startedAt);
|
|
3320
|
+
const lines = sessions.map((s) => {
|
|
3321
|
+
const label = s.name ? truncateMiddle2(s.name, 80) : truncateMiddle2(s.command, 120);
|
|
3322
|
+
return `${s.sessionId} ${pad2(s.status, 9)} ${formatDuration2(s.runtimeMs)} :: ${label}`;
|
|
3323
|
+
});
|
|
3324
|
+
return {
|
|
3325
|
+
success: true,
|
|
3326
|
+
message: lines.join(`
|
|
3327
|
+
`) || "No running or recent sessions.",
|
|
3328
|
+
data: { sessions }
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
if (!params.sessionId) {
|
|
3332
|
+
return {
|
|
3333
|
+
success: false,
|
|
3334
|
+
message: "sessionId is required for this action."
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
const session = getSession(params.sessionId);
|
|
3338
|
+
const finished = getFinishedSession(params.sessionId);
|
|
3339
|
+
const scopedSession = isInScope(session) ? session : undefined;
|
|
3340
|
+
const scopedFinished = isInScope(finished) ? finished : undefined;
|
|
3341
|
+
switch (params.action) {
|
|
3342
|
+
case "poll": {
|
|
3343
|
+
if (!scopedSession) {
|
|
3344
|
+
if (scopedFinished) {
|
|
3345
|
+
return {
|
|
3346
|
+
success: true,
|
|
3347
|
+
message: (scopedFinished.tail || `(no output recorded${scopedFinished.truncated ? " — truncated to cap" : ""})`) + `
|
|
3348
|
+
|
|
3349
|
+
Process exited with ${scopedFinished.exitSignal ? `signal ${scopedFinished.exitSignal}` : `code ${scopedFinished.exitCode ?? 0}`}.`,
|
|
3350
|
+
data: {
|
|
3351
|
+
status: scopedFinished.status === "completed" ? "completed" : "failed",
|
|
3352
|
+
sessionId: params.sessionId,
|
|
3353
|
+
exitCode: scopedFinished.exitCode ?? undefined,
|
|
3354
|
+
aggregated: scopedFinished.aggregated,
|
|
3355
|
+
name: deriveSessionName2(scopedFinished.command)
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
return {
|
|
3360
|
+
success: false,
|
|
3361
|
+
message: `No session found for ${params.sessionId}`
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
if (!scopedSession.backgrounded) {
|
|
3365
|
+
return {
|
|
3366
|
+
success: false,
|
|
3367
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
const { stdout, stderr } = drainSession(scopedSession);
|
|
3371
|
+
const exited = scopedSession.exited;
|
|
3372
|
+
const exitCode = scopedSession.exitCode ?? 0;
|
|
3373
|
+
const exitSignal = scopedSession.exitSignal ?? undefined;
|
|
3374
|
+
if (exited) {
|
|
3375
|
+
const status2 = exitCode === 0 && exitSignal == null ? "completed" : "failed";
|
|
3376
|
+
markExited(scopedSession, scopedSession.exitCode ?? null, scopedSession.exitSignal ?? null, status2);
|
|
3377
|
+
}
|
|
3378
|
+
const status = exited ? exitCode === 0 && exitSignal == null ? "completed" : "failed" : "running";
|
|
3379
|
+
const output = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join(`
|
|
3380
|
+
`).trim();
|
|
3381
|
+
return {
|
|
3382
|
+
success: true,
|
|
3383
|
+
message: (output || "(no new output)") + (exited ? `
|
|
3384
|
+
|
|
3385
|
+
Process exited with ${exitSignal ? `signal ${exitSignal}` : `code ${exitCode}`}.` : `
|
|
3386
|
+
|
|
3387
|
+
Process still running.`),
|
|
3388
|
+
data: {
|
|
3389
|
+
status,
|
|
3390
|
+
sessionId: params.sessionId,
|
|
3391
|
+
exitCode: exited ? exitCode : undefined,
|
|
3392
|
+
aggregated: scopedSession.aggregated,
|
|
3393
|
+
name: deriveSessionName2(scopedSession.command)
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
case "log": {
|
|
3398
|
+
if (scopedSession) {
|
|
3399
|
+
if (!scopedSession.backgrounded) {
|
|
3400
|
+
return {
|
|
3401
|
+
success: false,
|
|
3402
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
const { slice, totalLines, totalChars } = sliceLogLines2(scopedSession.aggregated, params.offset, params.limit);
|
|
3406
|
+
return {
|
|
3407
|
+
success: true,
|
|
3408
|
+
message: slice || "(no output yet)",
|
|
3409
|
+
data: {
|
|
3410
|
+
status: scopedSession.exited ? "completed" : "running",
|
|
3411
|
+
sessionId: params.sessionId,
|
|
3412
|
+
totalLines,
|
|
3413
|
+
totalChars,
|
|
3414
|
+
truncated: scopedSession.truncated,
|
|
3415
|
+
name: deriveSessionName2(scopedSession.command)
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
if (scopedFinished) {
|
|
3420
|
+
const { slice, totalLines, totalChars } = sliceLogLines2(scopedFinished.aggregated, params.offset, params.limit);
|
|
3421
|
+
const status = scopedFinished.status === "completed" ? "completed" : "failed";
|
|
3422
|
+
return {
|
|
3423
|
+
success: true,
|
|
3424
|
+
message: slice || "(no output recorded)",
|
|
3425
|
+
data: {
|
|
3426
|
+
status,
|
|
3427
|
+
sessionId: params.sessionId,
|
|
3428
|
+
totalLines,
|
|
3429
|
+
totalChars,
|
|
3430
|
+
truncated: scopedFinished.truncated,
|
|
3431
|
+
exitCode: scopedFinished.exitCode ?? undefined,
|
|
3432
|
+
exitSignal: scopedFinished.exitSignal ?? undefined,
|
|
3433
|
+
name: deriveSessionName2(scopedFinished.command)
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
return {
|
|
3438
|
+
success: false,
|
|
3439
|
+
message: `No session found for ${params.sessionId}`
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
case "write": {
|
|
3443
|
+
if (!scopedSession) {
|
|
3444
|
+
return {
|
|
3445
|
+
success: false,
|
|
3446
|
+
message: `No active session found for ${params.sessionId}`
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
if (!scopedSession.backgrounded) {
|
|
3450
|
+
return {
|
|
3451
|
+
success: false,
|
|
3452
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;
|
|
3456
|
+
if (!stdin || stdin.destroyed) {
|
|
3457
|
+
return {
|
|
3458
|
+
success: false,
|
|
3459
|
+
message: `Session ${params.sessionId} stdin is not writable.`
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
await new Promise((resolve, reject) => {
|
|
3463
|
+
stdin.write(params.data ?? "", (err) => {
|
|
3464
|
+
if (err)
|
|
3465
|
+
reject(err);
|
|
3466
|
+
else
|
|
3467
|
+
resolve();
|
|
3468
|
+
});
|
|
3469
|
+
});
|
|
3470
|
+
if (params.eof) {
|
|
3471
|
+
stdin.end();
|
|
3472
|
+
}
|
|
3473
|
+
return {
|
|
3474
|
+
success: true,
|
|
3475
|
+
message: `Wrote ${(params.data ?? "").length} bytes to session ${params.sessionId}${params.eof ? " (stdin closed)" : ""}.`,
|
|
3476
|
+
data: {
|
|
3477
|
+
sessionId: params.sessionId,
|
|
3478
|
+
name: deriveSessionName2(scopedSession.command)
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
case "send-keys": {
|
|
3483
|
+
if (!scopedSession) {
|
|
3484
|
+
return {
|
|
3485
|
+
success: false,
|
|
3486
|
+
message: `No active session found for ${params.sessionId}`
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
if (!scopedSession.backgrounded) {
|
|
3490
|
+
return {
|
|
3491
|
+
success: false,
|
|
3492
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;
|
|
3496
|
+
if (!stdin || stdin.destroyed) {
|
|
3497
|
+
return {
|
|
3498
|
+
success: false,
|
|
3499
|
+
message: `Session ${params.sessionId} stdin is not writable.`
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
const { data, warnings } = encodeKeySequence2({
|
|
3503
|
+
keys: params.keys,
|
|
3504
|
+
hex: params.hex,
|
|
3505
|
+
literal: params.literal
|
|
3506
|
+
});
|
|
3507
|
+
if (!data) {
|
|
3508
|
+
return { success: false, message: "No key data provided." };
|
|
3509
|
+
}
|
|
3510
|
+
await new Promise((resolve, reject) => {
|
|
3511
|
+
stdin.write(data, (err) => {
|
|
3512
|
+
if (err)
|
|
3513
|
+
reject(err);
|
|
3514
|
+
else
|
|
3515
|
+
resolve();
|
|
3516
|
+
});
|
|
3517
|
+
});
|
|
3518
|
+
return {
|
|
3519
|
+
success: true,
|
|
3520
|
+
message: `Sent ${data.length} bytes to session ${params.sessionId}.` + (warnings.length ? `
|
|
3521
|
+
Warnings:
|
|
3522
|
+
- ${warnings.join(`
|
|
3523
|
+
- `)}` : ""),
|
|
3524
|
+
data: {
|
|
3525
|
+
sessionId: params.sessionId,
|
|
3526
|
+
name: deriveSessionName2(scopedSession.command)
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
case "submit": {
|
|
3531
|
+
if (!scopedSession) {
|
|
3532
|
+
return {
|
|
3533
|
+
success: false,
|
|
3534
|
+
message: `No active session found for ${params.sessionId}`
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
if (!scopedSession.backgrounded) {
|
|
3538
|
+
return {
|
|
3539
|
+
success: false,
|
|
3540
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;
|
|
3544
|
+
if (!stdin || stdin.destroyed) {
|
|
3545
|
+
return {
|
|
3546
|
+
success: false,
|
|
3547
|
+
message: `Session ${params.sessionId} stdin is not writable.`
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
await new Promise((resolve, reject) => {
|
|
3551
|
+
stdin.write("\r", (err) => {
|
|
3552
|
+
if (err)
|
|
3553
|
+
reject(err);
|
|
3554
|
+
else
|
|
3555
|
+
resolve();
|
|
3556
|
+
});
|
|
3557
|
+
});
|
|
3558
|
+
return {
|
|
3559
|
+
success: true,
|
|
3560
|
+
message: `Submitted session ${params.sessionId} (sent CR).`,
|
|
3561
|
+
data: {
|
|
3562
|
+
sessionId: params.sessionId,
|
|
3563
|
+
name: deriveSessionName2(scopedSession.command)
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
case "paste": {
|
|
3568
|
+
if (!scopedSession) {
|
|
3569
|
+
return {
|
|
3570
|
+
success: false,
|
|
3571
|
+
message: `No active session found for ${params.sessionId}`
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
if (!scopedSession.backgrounded) {
|
|
3575
|
+
return {
|
|
3576
|
+
success: false,
|
|
3577
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;
|
|
3581
|
+
if (!stdin || stdin.destroyed) {
|
|
3582
|
+
return {
|
|
3583
|
+
success: false,
|
|
3584
|
+
message: `Session ${params.sessionId} stdin is not writable.`
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
3587
|
+
const payload = encodePaste2(params.text ?? "", params.bracketed !== false);
|
|
3588
|
+
if (!payload) {
|
|
3589
|
+
return { success: false, message: "No paste text provided." };
|
|
3590
|
+
}
|
|
3591
|
+
await new Promise((resolve, reject) => {
|
|
3592
|
+
stdin.write(payload, (err) => {
|
|
3593
|
+
if (err)
|
|
3594
|
+
reject(err);
|
|
3595
|
+
else
|
|
3596
|
+
resolve();
|
|
3597
|
+
});
|
|
3598
|
+
});
|
|
3599
|
+
return {
|
|
3600
|
+
success: true,
|
|
3601
|
+
message: `Pasted ${params.text?.length ?? 0} chars to session ${params.sessionId}.`,
|
|
3602
|
+
data: {
|
|
3603
|
+
sessionId: params.sessionId,
|
|
3604
|
+
name: deriveSessionName2(scopedSession.command)
|
|
3605
|
+
}
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
case "kill": {
|
|
3609
|
+
if (!scopedSession) {
|
|
3610
|
+
return {
|
|
3611
|
+
success: false,
|
|
3612
|
+
message: `No active session found for ${params.sessionId}`
|
|
3613
|
+
};
|
|
3614
|
+
}
|
|
3615
|
+
if (!scopedSession.backgrounded) {
|
|
3616
|
+
return {
|
|
3617
|
+
success: false,
|
|
3618
|
+
message: `Session ${params.sessionId} is not backgrounded.`
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
killSession2(scopedSession);
|
|
3622
|
+
markExited(scopedSession, null, "SIGKILL", "failed");
|
|
3623
|
+
return {
|
|
3624
|
+
success: true,
|
|
3625
|
+
message: `Killed session ${params.sessionId}.`,
|
|
3626
|
+
data: { name: deriveSessionName2(scopedSession.command) }
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
case "clear": {
|
|
3630
|
+
if (scopedFinished) {
|
|
3631
|
+
deleteSession(params.sessionId);
|
|
3632
|
+
return {
|
|
3633
|
+
success: true,
|
|
3634
|
+
message: `Cleared session ${params.sessionId}.`
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
return {
|
|
3638
|
+
success: false,
|
|
3639
|
+
message: `No finished session found for ${params.sessionId}`
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
case "remove": {
|
|
3643
|
+
if (scopedSession) {
|
|
3644
|
+
killSession2(scopedSession);
|
|
3645
|
+
markExited(scopedSession, null, "SIGKILL", "failed");
|
|
3646
|
+
return {
|
|
3647
|
+
success: true,
|
|
3648
|
+
message: `Removed session ${params.sessionId}.`,
|
|
3649
|
+
data: { name: deriveSessionName2(scopedSession.command) }
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
if (scopedFinished) {
|
|
3653
|
+
deleteSession(params.sessionId);
|
|
3654
|
+
return {
|
|
3655
|
+
success: true,
|
|
3656
|
+
message: `Removed session ${params.sessionId}.`
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
return {
|
|
3660
|
+
success: false,
|
|
3661
|
+
message: `No session found for ${params.sessionId}`
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return {
|
|
3666
|
+
success: false,
|
|
3667
|
+
message: `Unknown action ${params.action}`
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
listRunningSessions() {
|
|
3671
|
+
const scopeKey = this.scopeKey;
|
|
3672
|
+
return listRunningSessions().filter((s) => !scopeKey || s.scopeKey === scopeKey);
|
|
3673
|
+
}
|
|
3674
|
+
listFinishedSessions() {
|
|
3675
|
+
const scopeKey = this.scopeKey;
|
|
3676
|
+
return listFinishedSessions().filter((s) => !scopeKey || s.scopeKey === scopeKey);
|
|
3677
|
+
}
|
|
3678
|
+
getSession(id) {
|
|
3679
|
+
const session = getSession(id);
|
|
3680
|
+
if (!session)
|
|
3681
|
+
return;
|
|
3682
|
+
if (this.scopeKey && session.scopeKey !== this.scopeKey)
|
|
3683
|
+
return;
|
|
3684
|
+
return session;
|
|
3685
|
+
}
|
|
3686
|
+
getFinishedSession(id) {
|
|
3687
|
+
const session = getFinishedSession(id);
|
|
3688
|
+
if (!session)
|
|
3689
|
+
return;
|
|
3690
|
+
if (this.scopeKey && session.scopeKey !== this.scopeKey)
|
|
3691
|
+
return;
|
|
3692
|
+
return session;
|
|
3693
|
+
}
|
|
3694
|
+
killSessionById(id) {
|
|
3695
|
+
const session = this.getSession(id);
|
|
3696
|
+
if (!session)
|
|
3697
|
+
return false;
|
|
3698
|
+
killSession2(session);
|
|
3699
|
+
markExited(session, null, "SIGKILL", "killed");
|
|
3700
|
+
return true;
|
|
3701
|
+
}
|
|
3702
|
+
getCommandHistory(conversationId, limit) {
|
|
3703
|
+
const history = this.commandHistory.get(conversationId) || [];
|
|
3704
|
+
if (limit && limit > 0) {
|
|
3705
|
+
return history.slice(-limit);
|
|
3706
|
+
}
|
|
3707
|
+
return history;
|
|
3708
|
+
}
|
|
3709
|
+
clearCommandHistory(conversationId) {
|
|
3710
|
+
this.commandHistory.delete(conversationId);
|
|
3711
|
+
logger6.info(`Cleared command history for conversation: ${conversationId}`);
|
|
3712
|
+
}
|
|
3713
|
+
getCurrentDirectory(_conversationId) {
|
|
3714
|
+
return this.currentDirectory;
|
|
3715
|
+
}
|
|
3716
|
+
setCurrentDirectory(directory) {
|
|
3717
|
+
const validatedPath = validatePath(directory, this.shellConfig.allowedDirectory, this.currentDirectory);
|
|
3718
|
+
if (!validatedPath) {
|
|
3719
|
+
return false;
|
|
3720
|
+
}
|
|
3721
|
+
this.currentDirectory = validatedPath;
|
|
3722
|
+
return true;
|
|
3723
|
+
}
|
|
3724
|
+
getAllowedDirectory() {
|
|
3725
|
+
return this.shellConfig.allowedDirectory;
|
|
3726
|
+
}
|
|
3727
|
+
getShellConfig() {
|
|
3728
|
+
return { ...this.shellConfig };
|
|
3729
|
+
}
|
|
3730
|
+
async handleCdCommand(command) {
|
|
3731
|
+
const parts = command.split(/\s+/);
|
|
3732
|
+
if (parts.length < 2) {
|
|
3733
|
+
this.currentDirectory = this.shellConfig.allowedDirectory;
|
|
3734
|
+
return {
|
|
3735
|
+
success: true,
|
|
3736
|
+
stdout: `Changed directory to: ${this.currentDirectory}`,
|
|
3737
|
+
stderr: "",
|
|
3738
|
+
exitCode: 0,
|
|
3739
|
+
executedIn: this.currentDirectory
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
const targetPath = parts.slice(1).join(" ");
|
|
3743
|
+
const validatedPath = validatePath(targetPath, this.shellConfig.allowedDirectory, this.currentDirectory);
|
|
3744
|
+
if (!validatedPath) {
|
|
3745
|
+
return {
|
|
3746
|
+
success: false,
|
|
3747
|
+
stdout: "",
|
|
3748
|
+
stderr: "Cannot navigate outside allowed directory",
|
|
3749
|
+
exitCode: 1,
|
|
3750
|
+
error: "Permission denied",
|
|
3751
|
+
executedIn: this.currentDirectory
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
this.currentDirectory = validatedPath;
|
|
3755
|
+
return {
|
|
3756
|
+
success: true,
|
|
3757
|
+
stdout: `Changed directory to: ${this.currentDirectory}`,
|
|
3758
|
+
stderr: "",
|
|
3759
|
+
exitCode: 0,
|
|
3760
|
+
executedIn: this.currentDirectory
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
async runCommandSimple(command) {
|
|
3764
|
+
return new Promise((resolve) => {
|
|
3765
|
+
const useShell = command.includes(">") || command.includes("<") || command.includes("|");
|
|
3766
|
+
let cmd;
|
|
3767
|
+
let args;
|
|
3768
|
+
if (useShell) {
|
|
3769
|
+
const shell = getShellConfig2();
|
|
3770
|
+
cmd = shell.shell;
|
|
3771
|
+
args = [...shell.args, command];
|
|
3772
|
+
logger6.info(`Executing shell command: ${cmd} ${shell.args.join(" ")} "${command}" in ${this.currentDirectory}`);
|
|
3773
|
+
} else {
|
|
3774
|
+
const parts = command.split(/\s+/);
|
|
3775
|
+
cmd = parts[0];
|
|
3776
|
+
args = parts.slice(1);
|
|
3777
|
+
logger6.info(`Executing command: ${cmd} ${args.join(" ")} in ${this.currentDirectory}`);
|
|
3778
|
+
}
|
|
3779
|
+
let stdout = "";
|
|
3780
|
+
let stderr = "";
|
|
3781
|
+
let timedOut = false;
|
|
3782
|
+
const child = spawn2(cmd, args, {
|
|
3783
|
+
cwd: this.currentDirectory,
|
|
3784
|
+
env: process.env,
|
|
3785
|
+
shell: false
|
|
3786
|
+
});
|
|
3787
|
+
const timeout = setTimeout(() => {
|
|
3788
|
+
timedOut = true;
|
|
3789
|
+
child.kill("SIGTERM");
|
|
3790
|
+
setTimeout(() => {
|
|
3791
|
+
if (!child.killed) {
|
|
3792
|
+
child.kill("SIGKILL");
|
|
3793
|
+
}
|
|
3794
|
+
}, 5000);
|
|
3795
|
+
}, this.shellConfig.timeout);
|
|
3796
|
+
if (child.stdout) {
|
|
3797
|
+
child.stdout.on("data", (data) => {
|
|
3798
|
+
stdout += data.toString();
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
if (child.stderr) {
|
|
3802
|
+
child.stderr.on("data", (data) => {
|
|
3803
|
+
stderr += data.toString();
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
child.on("exit", (code) => {
|
|
3807
|
+
clearTimeout(timeout);
|
|
3808
|
+
if (timedOut) {
|
|
3809
|
+
resolve({
|
|
3810
|
+
success: false,
|
|
3811
|
+
stdout,
|
|
3812
|
+
stderr: `${stderr}
|
|
3813
|
+
Command timed out`,
|
|
3814
|
+
exitCode: code,
|
|
3815
|
+
error: "Command execution timeout",
|
|
3816
|
+
executedIn: this.currentDirectory
|
|
3817
|
+
});
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
resolve({
|
|
3821
|
+
success: code === 0,
|
|
3822
|
+
stdout,
|
|
3823
|
+
stderr,
|
|
3824
|
+
exitCode: code,
|
|
3825
|
+
executedIn: this.currentDirectory
|
|
3826
|
+
});
|
|
3827
|
+
});
|
|
3828
|
+
child.on("error", (err) => {
|
|
3829
|
+
clearTimeout(timeout);
|
|
3830
|
+
resolve({
|
|
3831
|
+
success: false,
|
|
3832
|
+
stdout,
|
|
3833
|
+
stderr: err.message,
|
|
3834
|
+
exitCode: 1,
|
|
3835
|
+
error: "Failed to execute command",
|
|
3836
|
+
executedIn: this.currentDirectory
|
|
3837
|
+
});
|
|
3838
|
+
});
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
async runExecProcess(opts) {
|
|
3842
|
+
const startedAt = Date.now();
|
|
3843
|
+
const sessionId = createSessionSlug();
|
|
3844
|
+
let child = null;
|
|
3845
|
+
let pty = null;
|
|
3846
|
+
let stdin;
|
|
3847
|
+
if (opts.usePty) {
|
|
3848
|
+
const { shell, args: shellArgs } = getShellConfig2();
|
|
3849
|
+
try {
|
|
3850
|
+
const ptyModule = await import("@lydell/node-pty");
|
|
3851
|
+
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
|
3852
|
+
if (!spawnPty) {
|
|
3853
|
+
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
|
3854
|
+
}
|
|
3855
|
+
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
|
3856
|
+
cwd: opts.workdir,
|
|
3857
|
+
env: opts.env,
|
|
3858
|
+
name: process.env.TERM ?? "xterm-256color",
|
|
3859
|
+
cols: 120,
|
|
3860
|
+
rows: 30
|
|
3861
|
+
});
|
|
3862
|
+
stdin = {
|
|
3863
|
+
destroyed: false,
|
|
3864
|
+
write: (data, cb) => {
|
|
3865
|
+
try {
|
|
3866
|
+
pty?.write(data);
|
|
3867
|
+
cb?.(null);
|
|
3868
|
+
} catch (err) {
|
|
3869
|
+
cb?.(err);
|
|
3870
|
+
}
|
|
3871
|
+
},
|
|
3872
|
+
end: () => {
|
|
3873
|
+
try {
|
|
3874
|
+
const eof = process.platform === "win32" ? "\x1A" : "\x04";
|
|
3875
|
+
pty?.write(eof);
|
|
3876
|
+
} catch {}
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
} catch (err) {
|
|
3880
|
+
const errText = String(err);
|
|
3881
|
+
const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY.`;
|
|
3882
|
+
logger6.warn(`exec: PTY spawn failed (${errText}); retrying without PTY.`);
|
|
3883
|
+
opts.warnings.push(warning);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
if (!pty) {
|
|
3887
|
+
const { shell, args: shellArgs } = getShellConfig2();
|
|
3888
|
+
const proc = spawn2(shell, [...shellArgs, opts.command], {
|
|
3889
|
+
cwd: opts.workdir,
|
|
3890
|
+
env: opts.env,
|
|
3891
|
+
detached: process.platform !== "win32",
|
|
3892
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3893
|
+
windowsHide: true
|
|
3894
|
+
});
|
|
3895
|
+
child = proc;
|
|
3896
|
+
stdin = child.stdin;
|
|
3897
|
+
}
|
|
3898
|
+
const session = {
|
|
3899
|
+
id: sessionId,
|
|
3900
|
+
command: opts.command,
|
|
3901
|
+
scopeKey: opts.scopeKey,
|
|
3902
|
+
sessionKey: opts.sessionKey,
|
|
3903
|
+
notifyOnExit: opts.notifyOnExit,
|
|
3904
|
+
exitNotified: false,
|
|
3905
|
+
child: child ?? undefined,
|
|
3906
|
+
stdin,
|
|
3907
|
+
pid: child?.pid ?? pty?.pid,
|
|
3908
|
+
startedAt,
|
|
3909
|
+
cwd: opts.workdir,
|
|
3910
|
+
maxOutputChars: opts.maxOutput,
|
|
3911
|
+
pendingMaxOutputChars: opts.pendingMaxOutput,
|
|
3912
|
+
totalOutputChars: 0,
|
|
3913
|
+
pendingStdout: [],
|
|
3914
|
+
pendingStderr: [],
|
|
3915
|
+
pendingStdoutChars: 0,
|
|
3916
|
+
pendingStderrChars: 0,
|
|
3917
|
+
aggregated: "",
|
|
3918
|
+
tail: "",
|
|
3919
|
+
exited: false,
|
|
3920
|
+
exitCode: undefined,
|
|
3921
|
+
exitSignal: undefined,
|
|
3922
|
+
truncated: false,
|
|
3923
|
+
backgrounded: false
|
|
3924
|
+
};
|
|
3925
|
+
addSession(session);
|
|
3926
|
+
let settled = false;
|
|
3927
|
+
let timeoutTimer = null;
|
|
3928
|
+
let timeoutFinalizeTimer = null;
|
|
3929
|
+
let timedOut = false;
|
|
3930
|
+
const timeoutFinalizeMs = 1000;
|
|
3931
|
+
let resolveFn = null;
|
|
3932
|
+
const settle = (outcome) => {
|
|
3933
|
+
if (settled) {
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3936
|
+
settled = true;
|
|
3937
|
+
resolveFn?.(outcome);
|
|
3938
|
+
};
|
|
3939
|
+
const finalizeTimeout = () => {
|
|
3940
|
+
if (session.exited) {
|
|
3941
|
+
return;
|
|
3942
|
+
}
|
|
3943
|
+
markExited(session, null, "SIGKILL", "failed");
|
|
3944
|
+
const aggregated = session.aggregated.trim();
|
|
3945
|
+
const reason = `Command timed out after ${opts.timeoutSec} seconds`;
|
|
3946
|
+
settle({
|
|
3947
|
+
status: "failed",
|
|
3948
|
+
exitCode: null,
|
|
3949
|
+
exitSignal: "SIGKILL",
|
|
3950
|
+
durationMs: Date.now() - startedAt,
|
|
3951
|
+
aggregated,
|
|
3952
|
+
timedOut: true,
|
|
3953
|
+
reason: aggregated ? `${aggregated}
|
|
3954
|
+
|
|
3955
|
+
${reason}` : reason
|
|
3956
|
+
});
|
|
3957
|
+
};
|
|
3958
|
+
const onTimeout = () => {
|
|
3959
|
+
timedOut = true;
|
|
3960
|
+
killSession2(session);
|
|
3961
|
+
if (!timeoutFinalizeTimer) {
|
|
3962
|
+
timeoutFinalizeTimer = setTimeout(() => {
|
|
3963
|
+
finalizeTimeout();
|
|
3964
|
+
}, timeoutFinalizeMs);
|
|
3965
|
+
}
|
|
3966
|
+
};
|
|
3967
|
+
if (opts.timeoutSec > 0) {
|
|
3968
|
+
timeoutTimer = setTimeout(() => {
|
|
3969
|
+
onTimeout();
|
|
3970
|
+
}, opts.timeoutSec * 1000);
|
|
3971
|
+
}
|
|
3972
|
+
const emitUpdate = () => {
|
|
3973
|
+
if (opts.onUpdate) {
|
|
3974
|
+
opts.onUpdate(session);
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3977
|
+
const handleStdout = (data) => {
|
|
3978
|
+
const str = sanitizeBinaryOutput2(data.toString());
|
|
3979
|
+
for (const chunk of chunkString2(str)) {
|
|
3980
|
+
appendOutput(session, "stdout", chunk);
|
|
3981
|
+
emitUpdate();
|
|
3982
|
+
}
|
|
3983
|
+
};
|
|
3984
|
+
const handleStderr = (data) => {
|
|
3985
|
+
const str = sanitizeBinaryOutput2(data.toString());
|
|
3986
|
+
for (const chunk of chunkString2(str)) {
|
|
3987
|
+
appendOutput(session, "stderr", chunk);
|
|
3988
|
+
emitUpdate();
|
|
3989
|
+
}
|
|
3990
|
+
};
|
|
3991
|
+
if (pty) {
|
|
3992
|
+
const cursorResponse = buildCursorPositionResponse2();
|
|
3993
|
+
pty.onData((data) => {
|
|
3994
|
+
const raw = data.toString();
|
|
3995
|
+
const { cleaned, requests } = stripDsrRequests2(raw);
|
|
3996
|
+
if (requests > 0) {
|
|
3997
|
+
for (let i = 0;i < requests; i += 1) {
|
|
3998
|
+
pty.write(cursorResponse);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
handleStdout(cleaned);
|
|
4002
|
+
});
|
|
4003
|
+
} else if (child) {
|
|
4004
|
+
child.stdout.on("data", handleStdout);
|
|
4005
|
+
child.stderr.on("data", handleStderr);
|
|
4006
|
+
}
|
|
4007
|
+
const promise = new Promise((resolve) => {
|
|
4008
|
+
resolveFn = resolve;
|
|
4009
|
+
const handleExit = (code, exitSignal) => {
|
|
4010
|
+
if (timeoutTimer) {
|
|
4011
|
+
clearTimeout(timeoutTimer);
|
|
4012
|
+
}
|
|
4013
|
+
if (timeoutFinalizeTimer) {
|
|
4014
|
+
clearTimeout(timeoutFinalizeTimer);
|
|
4015
|
+
}
|
|
4016
|
+
const durationMs = Date.now() - startedAt;
|
|
4017
|
+
const wasSignal = exitSignal != null;
|
|
4018
|
+
const isSuccess = code === 0 && !wasSignal && !timedOut;
|
|
4019
|
+
const status = isSuccess ? "completed" : "failed";
|
|
4020
|
+
markExited(session, code, exitSignal, status);
|
|
4021
|
+
if (!session.child && session.stdin) {
|
|
4022
|
+
session.stdin.destroyed = true;
|
|
4023
|
+
}
|
|
4024
|
+
if (settled) {
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const aggregated = session.aggregated.trim();
|
|
4028
|
+
if (!isSuccess) {
|
|
4029
|
+
const reason = timedOut ? `Command timed out after ${opts.timeoutSec} seconds` : wasSignal && exitSignal ? `Command aborted by signal ${exitSignal}` : code === null ? "Command aborted before exit code was captured" : `Command exited with code ${code}`;
|
|
4030
|
+
const message = aggregated ? `${aggregated}
|
|
4031
|
+
|
|
4032
|
+
${reason}` : reason;
|
|
4033
|
+
settle({
|
|
4034
|
+
status: "failed",
|
|
4035
|
+
exitCode: code ?? null,
|
|
4036
|
+
exitSignal: exitSignal ?? null,
|
|
4037
|
+
durationMs,
|
|
4038
|
+
aggregated,
|
|
4039
|
+
timedOut,
|
|
4040
|
+
reason: message
|
|
4041
|
+
});
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
settle({
|
|
4045
|
+
status: "completed",
|
|
4046
|
+
exitCode: code,
|
|
4047
|
+
exitSignal: exitSignal ?? null,
|
|
4048
|
+
durationMs,
|
|
4049
|
+
aggregated,
|
|
4050
|
+
timedOut: false
|
|
4051
|
+
});
|
|
4052
|
+
};
|
|
4053
|
+
if (pty) {
|
|
4054
|
+
pty.onExit((event) => {
|
|
4055
|
+
const rawSignal = event.signal ?? null;
|
|
4056
|
+
const normalizedSignal = rawSignal === 0 ? null : rawSignal;
|
|
4057
|
+
handleExit(event.exitCode, normalizedSignal);
|
|
4058
|
+
});
|
|
4059
|
+
} else if (child) {
|
|
4060
|
+
child.once("close", (code, exitSignal) => {
|
|
4061
|
+
handleExit(code, exitSignal);
|
|
4062
|
+
});
|
|
4063
|
+
child.once("error", (err) => {
|
|
4064
|
+
if (timeoutTimer) {
|
|
4065
|
+
clearTimeout(timeoutTimer);
|
|
4066
|
+
}
|
|
4067
|
+
if (timeoutFinalizeTimer) {
|
|
4068
|
+
clearTimeout(timeoutFinalizeTimer);
|
|
4069
|
+
}
|
|
4070
|
+
markExited(session, null, null, "failed");
|
|
4071
|
+
const aggregated = session.aggregated.trim();
|
|
4072
|
+
const message = aggregated ? `${aggregated}
|
|
4073
|
+
|
|
4074
|
+
${String(err)}` : String(err);
|
|
4075
|
+
settle({
|
|
4076
|
+
status: "failed",
|
|
4077
|
+
exitCode: null,
|
|
4078
|
+
exitSignal: null,
|
|
4079
|
+
durationMs: Date.now() - startedAt,
|
|
4080
|
+
aggregated,
|
|
4081
|
+
timedOut,
|
|
4082
|
+
reason: message
|
|
4083
|
+
});
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
});
|
|
4087
|
+
return {
|
|
4088
|
+
session,
|
|
4089
|
+
startedAt,
|
|
4090
|
+
promise,
|
|
4091
|
+
kill: () => killSession2(session)
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
4094
|
+
addToHistory(conversationId, command, result, fileOperations) {
|
|
4095
|
+
if (!conversationId)
|
|
4096
|
+
return;
|
|
4097
|
+
const historyEntry = {
|
|
4098
|
+
command,
|
|
4099
|
+
stdout: result.stdout,
|
|
4100
|
+
stderr: result.stderr,
|
|
4101
|
+
exitCode: result.exitCode,
|
|
4102
|
+
timestamp: Date.now(),
|
|
4103
|
+
workingDirectory: result.executedIn,
|
|
4104
|
+
fileOperations
|
|
4105
|
+
};
|
|
4106
|
+
if (!this.commandHistory.has(conversationId)) {
|
|
4107
|
+
this.commandHistory.set(conversationId, []);
|
|
4108
|
+
}
|
|
4109
|
+
const history = this.commandHistory.get(conversationId);
|
|
4110
|
+
if (!history) {
|
|
4111
|
+
throw new Error(`No history found for conversation ${conversationId}`);
|
|
4112
|
+
}
|
|
4113
|
+
history.push(historyEntry);
|
|
4114
|
+
if (history.length > this.maxHistoryPerConversation) {
|
|
4115
|
+
history.shift();
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
detectFileOperations(command, cwd) {
|
|
4119
|
+
const operations = [];
|
|
4120
|
+
const parts = command.trim().split(/\s+/);
|
|
4121
|
+
const cmd = parts[0].toLowerCase();
|
|
4122
|
+
if (cmd === "touch" && parts.length > 1) {
|
|
4123
|
+
operations.push({
|
|
4124
|
+
type: "create",
|
|
4125
|
+
target: this.resolvePath(parts[1], cwd)
|
|
4126
|
+
});
|
|
4127
|
+
} else if (cmd === "echo" && command.includes(">")) {
|
|
4128
|
+
const match = command.match(/>\s*([^\s]+)$/);
|
|
4129
|
+
if (match) {
|
|
4130
|
+
operations.push({
|
|
4131
|
+
type: "write",
|
|
4132
|
+
target: this.resolvePath(match[1], cwd)
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
} else if (cmd === "mkdir" && parts.length > 1) {
|
|
4136
|
+
operations.push({
|
|
4137
|
+
type: "mkdir",
|
|
4138
|
+
target: this.resolvePath(parts[1], cwd)
|
|
4139
|
+
});
|
|
4140
|
+
} else if (cmd === "cat" && parts.length > 1 && !command.includes(">")) {
|
|
4141
|
+
operations.push({
|
|
4142
|
+
type: "read",
|
|
4143
|
+
target: this.resolvePath(parts[1], cwd)
|
|
4144
|
+
});
|
|
4145
|
+
} else if (cmd === "mv" && parts.length > 2) {
|
|
4146
|
+
operations.push({
|
|
4147
|
+
type: "move",
|
|
4148
|
+
target: this.resolvePath(parts[1], cwd),
|
|
4149
|
+
secondaryTarget: this.resolvePath(parts[2], cwd)
|
|
4150
|
+
});
|
|
4151
|
+
} else if (cmd === "cp" && parts.length > 2) {
|
|
4152
|
+
operations.push({
|
|
4153
|
+
type: "copy",
|
|
4154
|
+
target: this.resolvePath(parts[1], cwd),
|
|
4155
|
+
secondaryTarget: this.resolvePath(parts[2], cwd)
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
return operations.length > 0 ? operations : undefined;
|
|
4159
|
+
}
|
|
4160
|
+
resolvePath(filePath, cwd) {
|
|
4161
|
+
if (path7.isAbsolute(filePath)) {
|
|
4162
|
+
return filePath;
|
|
4163
|
+
}
|
|
4164
|
+
return path7.join(cwd, filePath);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
// index.ts
|
|
4169
|
+
function terminalSupportedByEnv(env) {
|
|
4170
|
+
const variant = (env.ELIZA_BUILD_VARIANT ?? "").trim().toLowerCase();
|
|
4171
|
+
if (variant === "store")
|
|
4172
|
+
return false;
|
|
4173
|
+
const platform = env.ELIZA_PLATFORM?.trim().toLowerCase();
|
|
4174
|
+
const mobile = platform === "android" || platform === "ios" || Boolean(env.ANDROID_ROOT || env.ANDROID_DATA);
|
|
4175
|
+
if (!mobile)
|
|
4176
|
+
return true;
|
|
4177
|
+
const mode = (env.ELIZA_RUNTIME_MODE ?? env.RUNTIME_MODE ?? env.LOCAL_RUNTIME_MODE ?? "").trim().toLowerCase();
|
|
4178
|
+
return platform === "android" && mode === "local-yolo";
|
|
4179
|
+
}
|
|
4180
|
+
var shellPlugin = {
|
|
4181
|
+
name: "shell",
|
|
4182
|
+
description: "Shell observability and history management providers",
|
|
4183
|
+
services: [ShellService, ExecApprovalService],
|
|
4184
|
+
actions: [],
|
|
4185
|
+
providers: [shellHistoryProvider],
|
|
4186
|
+
async dispose(runtime) {
|
|
4187
|
+
await runtime.getService(ShellService.serviceType)?.stop();
|
|
4188
|
+
await runtime.getService(ExecApprovalService.serviceType)?.stop();
|
|
4189
|
+
},
|
|
4190
|
+
autoEnable: {
|
|
4191
|
+
shouldEnable: (env, config) => {
|
|
4192
|
+
const f = config.features?.shell;
|
|
4193
|
+
return (f === true || typeof f === "object" && f !== null && f.enabled !== false) && terminalSupportedByEnv(env);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
};
|
|
4197
|
+
var plugin_shell_default = shellPlugin;
|
|
4198
|
+
export {
|
|
4199
|
+
validatePath,
|
|
4200
|
+
truncateMiddle2 as truncateMiddle,
|
|
4201
|
+
trimWithCap,
|
|
4202
|
+
tail,
|
|
4203
|
+
stripDsrRequests2 as stripDsrRequests,
|
|
4204
|
+
spawnWithFallback,
|
|
4205
|
+
sliceUtf16Safe2 as sliceUtf16Safe,
|
|
4206
|
+
sliceLogLines2 as sliceLogLines,
|
|
4207
|
+
shellPlugin,
|
|
4208
|
+
shellHistoryProvider,
|
|
4209
|
+
setJobTtlMs,
|
|
4210
|
+
saveApprovals,
|
|
4211
|
+
sanitizeBinaryOutput2 as sanitizeBinaryOutput,
|
|
4212
|
+
resolveWorkdir2 as resolveWorkdir,
|
|
4213
|
+
resolveTerminalShell,
|
|
4214
|
+
resolveSafeBins,
|
|
4215
|
+
resolveExecutable,
|
|
4216
|
+
resolveCommandResolution,
|
|
4217
|
+
resolveCommandFromArgv,
|
|
4218
|
+
resolveApprovalsFromFile,
|
|
4219
|
+
resolveApprovals,
|
|
4220
|
+
resetProcessRegistryForTests,
|
|
4221
|
+
requiresExecApproval,
|
|
4222
|
+
recordAllowlistUse,
|
|
4223
|
+
readEnvInt2 as readEnvInt,
|
|
4224
|
+
readApprovalsSnapshot,
|
|
4225
|
+
pad2 as pad,
|
|
4226
|
+
normalizeSafeBins,
|
|
4227
|
+
normalizeApprovals,
|
|
4228
|
+
missingToolMessage,
|
|
4229
|
+
missingTerminalToolForCommand,
|
|
4230
|
+
minSecurity,
|
|
4231
|
+
maxAsk,
|
|
4232
|
+
matchAllowlist,
|
|
4233
|
+
markExited,
|
|
4234
|
+
markBackgrounded,
|
|
4235
|
+
loadShellConfig,
|
|
4236
|
+
loadApprovals,
|
|
4237
|
+
listRunningSessions,
|
|
4238
|
+
listFinishedSessions,
|
|
4239
|
+
killSession2 as killSession,
|
|
4240
|
+
killProcessTree2 as killProcessTree,
|
|
4241
|
+
isSafeCommand,
|
|
4242
|
+
isSafeBinUsage,
|
|
4243
|
+
isForbiddenCommand,
|
|
4244
|
+
isAndroidRuntime,
|
|
4245
|
+
getShellConfig2 as getShellConfig,
|
|
4246
|
+
getSession,
|
|
4247
|
+
getFinishedSession,
|
|
4248
|
+
getApprovalSocketPath,
|
|
4249
|
+
getApprovalFilePath,
|
|
4250
|
+
formatTerminalCapabilities,
|
|
4251
|
+
formatSpawnError,
|
|
4252
|
+
formatDuration2 as formatDuration,
|
|
4253
|
+
extractBaseCommand,
|
|
4254
|
+
evaluateShellAllowlist,
|
|
4255
|
+
evaluateExecAllowlist,
|
|
4256
|
+
ensureApprovals,
|
|
4257
|
+
encodePaste2 as encodePaste,
|
|
4258
|
+
encodeKeySequence2 as encodeKeySequence,
|
|
4259
|
+
drainSession,
|
|
4260
|
+
detectTerminalCapabilities,
|
|
4261
|
+
deriveSessionName2 as deriveSessionName,
|
|
4262
|
+
deleteSession,
|
|
4263
|
+
plugin_shell_default as default,
|
|
4264
|
+
createSessionSlug,
|
|
4265
|
+
coerceEnv2 as coerceEnv,
|
|
4266
|
+
clearFinished,
|
|
4267
|
+
clampNumber2 as clampNumber,
|
|
4268
|
+
chunkString2 as chunkString,
|
|
4269
|
+
buildCursorPositionResponse2 as buildCursorPositionResponse,
|
|
4270
|
+
appendOutput,
|
|
4271
|
+
analyzeShellCommand,
|
|
4272
|
+
addSession,
|
|
4273
|
+
addAllowlistEntry,
|
|
4274
|
+
TERMINAL_TOOL_NAMES,
|
|
4275
|
+
ShellService,
|
|
4276
|
+
ExecApprovalService,
|
|
4277
|
+
EXEC_APPROVAL_DEFAULTS,
|
|
4278
|
+
DEFAULT_SAFE_BINS,
|
|
4279
|
+
DEFAULT_FORBIDDEN_COMMANDS,
|
|
4280
|
+
BRACKETED_PASTE_START2 as BRACKETED_PASTE_START,
|
|
4281
|
+
BRACKETED_PASTE_END2 as BRACKETED_PASTE_END
|
|
4282
|
+
};
|
|
4283
|
+
|
|
4284
|
+
//# debugId=84725595753D266B64756E2164756E21
|