arisa 4.0.22 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +33 -58
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +69 -12
- package/src/core/agent/core-tools.js +139 -0
- package/src/core/agent/runtime-context.js +14 -3
- package/src/core/agent/system-shell-tool.js +207 -0
- package/src/index.js +44 -10
- package/src/runtime/bootstrap.js +412 -36
- package/src/runtime/create-app.js +52 -9
- package/src/transport/telegram/bot.js +4 -3
- package/test/agent-tool-policy.test.js +91 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { constants, existsSync } from "node:fs";
|
|
5
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
|
|
8
|
+
const maxOutputBytes = 50 * 1024;
|
|
9
|
+
const defaultTimeoutMs = 60_000;
|
|
10
|
+
|
|
11
|
+
function isPowerShell(shellPath) {
|
|
12
|
+
const name = path.basename(shellPath).toLowerCase();
|
|
13
|
+
return name === "powershell.exe" || name === "powershell" || name === "pwsh.exe" || name === "pwsh";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isCmd(shellPath) {
|
|
17
|
+
const name = path.basename(shellPath).toLowerCase();
|
|
18
|
+
return name === "cmd.exe" || name === "cmd";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveNativeShell(shellPath = "") {
|
|
22
|
+
if (shellPath) {
|
|
23
|
+
if (isPowerShell(shellPath)) {
|
|
24
|
+
return {
|
|
25
|
+
shell: shellPath,
|
|
26
|
+
label: "powershell",
|
|
27
|
+
argsFor: (command) => ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (isCmd(shellPath)) {
|
|
31
|
+
return {
|
|
32
|
+
shell: shellPath,
|
|
33
|
+
label: "cmd",
|
|
34
|
+
argsFor: (command) => ["/d", "/s", "/c", command]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
shell: shellPath,
|
|
39
|
+
label: "sh",
|
|
40
|
+
argsFor: (command) => ["-lc", command]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.platform === "win32") {
|
|
45
|
+
return {
|
|
46
|
+
shell: "powershell.exe",
|
|
47
|
+
label: "powershell",
|
|
48
|
+
argsFor: (command) => ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
shell: existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh",
|
|
54
|
+
label: "sh",
|
|
55
|
+
argsFor: (command) => ["-lc", command]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function appendLimited(current, chunk, state) {
|
|
60
|
+
state.totalBytes += chunk.length;
|
|
61
|
+
|
|
62
|
+
const remaining = maxOutputBytes - state.storedBytes;
|
|
63
|
+
if (remaining <= 0) {
|
|
64
|
+
state.truncated = true;
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const accepted = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
69
|
+
state.storedBytes += accepted.length;
|
|
70
|
+
if (accepted.length < chunk.length) {
|
|
71
|
+
state.truncated = true;
|
|
72
|
+
}
|
|
73
|
+
return current + accepted.toString("utf8");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function killProcessTree(child) {
|
|
77
|
+
if (!child.pid) return;
|
|
78
|
+
if (process.platform === "win32") {
|
|
79
|
+
spawn("taskkill", ["/F", "/T", "/PID", String(child.pid)], {
|
|
80
|
+
stdio: "ignore",
|
|
81
|
+
windowsHide: true
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
process.kill(-child.pid, "SIGTERM");
|
|
88
|
+
} catch {
|
|
89
|
+
try {
|
|
90
|
+
child.kill("SIGTERM");
|
|
91
|
+
} catch {
|
|
92
|
+
// Process already exited.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatOutput({ stdout, stderr, exitCode, timedOut, truncated }) {
|
|
98
|
+
const sections = [];
|
|
99
|
+
if (stdout.trim()) sections.push(`stdout:\n${stdout.trimEnd()}`);
|
|
100
|
+
if (stderr.trim()) sections.push(`stderr:\n${stderr.trimEnd()}`);
|
|
101
|
+
if (!sections.length) sections.push(exitCode === 0 && !timedOut ? "(Success, no output)" : "(No output)");
|
|
102
|
+
if (timedOut) sections.push("Command timed out.");
|
|
103
|
+
if (truncated) sections.push(`[Output truncated to ${maxOutputBytes} bytes.]`);
|
|
104
|
+
return sections.join("\n\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function runShellCommand({ command, cwd, shellPath, timeoutMs }) {
|
|
108
|
+
await access(cwd, constants.F_OK);
|
|
109
|
+
const nativeShell = resolveNativeShell(shellPath);
|
|
110
|
+
const child = spawn(nativeShell.shell, nativeShell.argsFor(command), {
|
|
111
|
+
cwd,
|
|
112
|
+
detached: process.platform !== "win32",
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
114
|
+
windowsHide: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
let stdout = "";
|
|
118
|
+
let stderr = "";
|
|
119
|
+
const outputState = {
|
|
120
|
+
storedBytes: 0,
|
|
121
|
+
totalBytes: 0,
|
|
122
|
+
truncated: false
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
timedOut = true;
|
|
128
|
+
killProcessTree(child);
|
|
129
|
+
}, timeoutMs);
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
child.stdout.on("data", (chunk) => {
|
|
133
|
+
stdout = appendLimited(stdout, chunk, outputState);
|
|
134
|
+
});
|
|
135
|
+
child.stderr.on("data", (chunk) => {
|
|
136
|
+
stderr = appendLimited(stderr, chunk, outputState);
|
|
137
|
+
});
|
|
138
|
+
child.on("error", (error) => {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
resolve({
|
|
141
|
+
ok: false,
|
|
142
|
+
error,
|
|
143
|
+
stdout,
|
|
144
|
+
stderr,
|
|
145
|
+
shell: nativeShell,
|
|
146
|
+
timedOut,
|
|
147
|
+
truncated: outputState.truncated,
|
|
148
|
+
totalBytes: outputState.totalBytes
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
child.on("close", (exitCode) => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve({
|
|
154
|
+
ok: !timedOut && exitCode === 0,
|
|
155
|
+
exitCode,
|
|
156
|
+
stdout,
|
|
157
|
+
stderr,
|
|
158
|
+
shell: nativeShell,
|
|
159
|
+
timedOut,
|
|
160
|
+
truncated: outputState.truncated,
|
|
161
|
+
totalBytes: outputState.totalBytes
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createSystemShellTool({ workspaceDir, shell = {} }) {
|
|
168
|
+
return defineTool({
|
|
169
|
+
name: "system_shell",
|
|
170
|
+
label: "System Shell",
|
|
171
|
+
description: "Run a command in the active Arisa workspace using the native system shell: PowerShell on Windows, and sh/bash-compatible shell on Unix.",
|
|
172
|
+
parameters: Type.Object({
|
|
173
|
+
command: Type.String({ description: "Command to execute in the active workspace." }),
|
|
174
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds for this command." }))
|
|
175
|
+
}),
|
|
176
|
+
execute: async (_id, params) => {
|
|
177
|
+
const timeoutMs = Number.isFinite(Number(params.timeoutMs)) && Number(params.timeoutMs) > 0
|
|
178
|
+
? Math.floor(Number(params.timeoutMs))
|
|
179
|
+
: (shell.timeoutMs || defaultTimeoutMs);
|
|
180
|
+
const result = await runShellCommand({
|
|
181
|
+
command: params.command,
|
|
182
|
+
cwd: workspaceDir,
|
|
183
|
+
shellPath: shell.shellPath,
|
|
184
|
+
timeoutMs
|
|
185
|
+
});
|
|
186
|
+
const details = {
|
|
187
|
+
stdout: result.stdout,
|
|
188
|
+
stderr: result.stderr,
|
|
189
|
+
exitCode: result.exitCode ?? null,
|
|
190
|
+
shell: result.shell.label,
|
|
191
|
+
shellPath: result.shell.shell,
|
|
192
|
+
cwd: workspaceDir,
|
|
193
|
+
timedOut: result.timedOut,
|
|
194
|
+
truncated: result.truncated,
|
|
195
|
+
totalBytes: result.totalBytes
|
|
196
|
+
};
|
|
197
|
+
if (result.error) {
|
|
198
|
+
details.error = result.error.message;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: "text", text: result.error?.message || formatOutput(result) }],
|
|
202
|
+
details,
|
|
203
|
+
isError: !result.ok
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
package/src/index.js
CHANGED
|
@@ -69,8 +69,20 @@ function toNestedOverrides(nestedFlags) {
|
|
|
69
69
|
|
|
70
70
|
function toServiceRunnerArgs(nestedFlags) {
|
|
71
71
|
const args = [];
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const serviceSafePiFlags = [
|
|
73
|
+
"pi.provider",
|
|
74
|
+
"pi.model",
|
|
75
|
+
"pi.workspaceDir",
|
|
76
|
+
"pi.tools",
|
|
77
|
+
"pi.excludeTools",
|
|
78
|
+
"pi.shellPath",
|
|
79
|
+
"pi.shellTimeoutMs"
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const flag of serviceSafePiFlags) {
|
|
83
|
+
if (nestedFlags[flag]) {
|
|
84
|
+
args.push(`--${flag}`, nestedFlags[flag]);
|
|
85
|
+
}
|
|
74
86
|
}
|
|
75
87
|
return args;
|
|
76
88
|
}
|
|
@@ -104,14 +116,34 @@ async function startRuntimeApp() {
|
|
|
104
116
|
await app.start();
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
async function startBackgroundService() {
|
|
120
|
+
const result = await startService({ verbose, cliArgs: toServiceRunnerArgs(cli.nestedFlags) });
|
|
121
|
+
if (!result.ok) {
|
|
122
|
+
console.log(`Arisa is already running in background (pid ${result.pid}).`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
console.log(`Arisa started in background (pid ${result.pid}).`);
|
|
126
|
+
console.log(`Log file: ${result.logFile}`);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
async function runForeground() {
|
|
108
131
|
const hasRuntimePiOverrides = Boolean(
|
|
109
132
|
runtimeOverrides?.pi?.model
|
|
110
133
|
|| runtimeOverrides?.pi?.provider
|
|
111
134
|
|| runtimeOverrides?.pi?.apiKey
|
|
135
|
+
|| runtimeOverrides?.pi?.workspaceDir
|
|
136
|
+
|| runtimeOverrides?.pi?.tools
|
|
137
|
+
|| runtimeOverrides?.pi?.excludeTools
|
|
138
|
+
|| runtimeOverrides?.pi?.shellPath
|
|
139
|
+
|| runtimeOverrides?.pi?.shellTimeoutMs
|
|
112
140
|
);
|
|
113
141
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
114
|
-
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
142
|
+
const bootstrapResult = await bootstrapIfNeeded({ force: forceBootstrap });
|
|
143
|
+
if (bootstrapResult.startInBackground) {
|
|
144
|
+
await startBackgroundService();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
115
147
|
try {
|
|
116
148
|
await startRuntimeApp();
|
|
117
149
|
} catch (error) {
|
|
@@ -126,7 +158,11 @@ async function runForeground() {
|
|
|
126
158
|
throw error;
|
|
127
159
|
}
|
|
128
160
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
129
|
-
await bootstrapIfNeeded({ force: true });
|
|
161
|
+
const retryBootstrapResult = await bootstrapIfNeeded({ force: true });
|
|
162
|
+
if (retryBootstrapResult.startInBackground) {
|
|
163
|
+
await startBackgroundService();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
130
166
|
await startRuntimeApp();
|
|
131
167
|
return;
|
|
132
168
|
}
|
|
@@ -142,14 +178,12 @@ async function main() {
|
|
|
142
178
|
}
|
|
143
179
|
|
|
144
180
|
if (command === "start") {
|
|
145
|
-
await bootstrapIfNeeded({ force: forceBootstrap });
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(`Arisa is already running in background (pid ${result.pid}).`);
|
|
181
|
+
const bootstrapResult = await bootstrapIfNeeded({ force: forceBootstrap });
|
|
182
|
+
if (bootstrapResult.configCreated && bootstrapResult.viaTelegram && !bootstrapResult.startInBackground) {
|
|
183
|
+
console.log("Config saved. Arisa was not started in background.");
|
|
149
184
|
return;
|
|
150
185
|
}
|
|
151
|
-
|
|
152
|
-
console.log(`Log file: ${result.logFile}`);
|
|
186
|
+
await startBackgroundService();
|
|
153
187
|
return;
|
|
154
188
|
}
|
|
155
189
|
|