@lazyingart/agintiflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -0
- package/LICENSE +201 -0
- package/README.md +294 -0
- package/bin/aginti-cli.js +7 -0
- package/docker/sandbox.Dockerfile +22 -0
- package/docs/npm-publishing.md +45 -0
- package/i18n/README.ar.md +51 -0
- package/i18n/README.de.md +51 -0
- package/i18n/README.es.md +51 -0
- package/i18n/README.fr.md +51 -0
- package/i18n/README.ja.md +51 -0
- package/i18n/README.ko.md +51 -0
- package/i18n/README.ru.md +51 -0
- package/i18n/README.vi.md +51 -0
- package/i18n/README.zh-Hans.md +51 -0
- package/i18n/README.zh-Hant.md +171 -0
- package/logos/banner-opaque.png +0 -0
- package/logos/logo.png +0 -0
- package/package.json +62 -0
- package/public/app.js +1060 -0
- package/public/index.html +198 -0
- package/public/styles.css +305 -0
- package/run.js +6 -0
- package/scripts/install-docker-ubuntu.sh +140 -0
- package/src/agent-runner.js +686 -0
- package/src/cli.js +181 -0
- package/src/command-policy.js +185 -0
- package/src/config.js +89 -0
- package/src/docker-sandbox.js +309 -0
- package/src/guardrails.js +122 -0
- package/src/model-client.js +212 -0
- package/src/model-routing.js +135 -0
- package/src/redaction.js +29 -0
- package/src/session-store.js +73 -0
- package/src/snapshot.js +55 -0
- package/src/tool-wrappers.js +193 -0
- package/src/web-db.js +158 -0
- package/web.js +530 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { evaluateCommandPolicy, normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
|
|
7
|
+
import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
8
|
+
|
|
9
|
+
const execFile = promisify(execFileCallback);
|
|
10
|
+
const DOCKER_WORKSPACE = "/workspace";
|
|
11
|
+
const READY_IMAGES = new Set();
|
|
12
|
+
const SANDBOX_LOG_LIMIT = 80;
|
|
13
|
+
const sandboxLogs = [];
|
|
14
|
+
|
|
15
|
+
function recordSandboxLog(type, data = {}) {
|
|
16
|
+
sandboxLogs.push({
|
|
17
|
+
at: new Date().toISOString(),
|
|
18
|
+
type,
|
|
19
|
+
data: redactValue(data),
|
|
20
|
+
});
|
|
21
|
+
while (sandboxLogs.length > SANDBOX_LOG_LIMIT) sandboxLogs.shift();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shellEscape(value) {
|
|
25
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildDockerInvocation(args) {
|
|
29
|
+
return ["docker", ...args].map(shellEscape).join(" ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function execDocker(args, options = {}) {
|
|
33
|
+
const execOptions = {
|
|
34
|
+
timeout: options.timeout ?? 30000,
|
|
35
|
+
maxBuffer: options.maxBuffer ?? 200 * 1024,
|
|
36
|
+
};
|
|
37
|
+
recordSandboxLog("docker.command", { args });
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await execFile("docker", args, execOptions);
|
|
41
|
+
return {
|
|
42
|
+
stdout: redactSensitiveText(result.stdout),
|
|
43
|
+
stderr: redactSensitiveText(result.stderr),
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (!/permission denied|Got permission denied|connect: permission denied/i.test(message)) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await execFile("sg", ["docker", "-c", buildDockerInvocation(args)], execOptions);
|
|
52
|
+
return {
|
|
53
|
+
stdout: redactSensitiveText(result.stdout),
|
|
54
|
+
stderr: redactSensitiveText(result.stderr),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function dockerAvailable() {
|
|
60
|
+
try {
|
|
61
|
+
await execDocker(["version", "--format", "{{.Server.Version}}"], { timeout: 8000, maxBuffer: 16 * 1024 });
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
recordSandboxLog("docker.unavailable", { error: error instanceof Error ? error.message : String(error) });
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function dockerImageExists(image) {
|
|
70
|
+
try {
|
|
71
|
+
await execDocker(["image", "inspect", image], {
|
|
72
|
+
timeout: 10000,
|
|
73
|
+
maxBuffer: 100 * 1024,
|
|
74
|
+
});
|
|
75
|
+
READY_IMAGES.add(image);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
READY_IMAGES.delete(image);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function pathAccess(targetPath, mode) {
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(targetPath, mode);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getSandboxLogs(limit = 30) {
|
|
93
|
+
return sandboxLogs.slice(-limit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getDockerSandboxStatus(config) {
|
|
97
|
+
const image = config.dockerSandboxImage;
|
|
98
|
+
const dockerfilePath = path.join(config.baseDir, "docker", "sandbox.Dockerfile");
|
|
99
|
+
const workspace = config.commandCwd;
|
|
100
|
+
const [dockerReady, dockerfileExists, workspaceExists, workspaceReadable, workspaceWritable] = await Promise.all([
|
|
101
|
+
dockerAvailable(),
|
|
102
|
+
pathAccess(dockerfilePath, fsConstants.R_OK),
|
|
103
|
+
pathAccess(workspace, fsConstants.F_OK),
|
|
104
|
+
pathAccess(workspace, fsConstants.R_OK),
|
|
105
|
+
pathAccess(workspace, fsConstants.W_OK),
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const imageReady = dockerReady ? await dockerImageExists(image) : false;
|
|
109
|
+
return {
|
|
110
|
+
sandboxMode: normalizeSandboxMode(config.sandboxMode),
|
|
111
|
+
useDockerSandbox: Boolean(config.useDockerSandbox),
|
|
112
|
+
packageInstallPolicy: normalizePackageInstallPolicy(config.packageInstallPolicy),
|
|
113
|
+
image,
|
|
114
|
+
dockerfilePath,
|
|
115
|
+
dockerAvailable: dockerReady,
|
|
116
|
+
imageReady,
|
|
117
|
+
workspace,
|
|
118
|
+
workspaceExists,
|
|
119
|
+
workspaceReadable,
|
|
120
|
+
workspaceWritable,
|
|
121
|
+
dockerfileExists,
|
|
122
|
+
logs: getSandboxLogs(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function buildDockerSandboxImage(config, observers) {
|
|
127
|
+
const image = config.dockerSandboxImage;
|
|
128
|
+
const dockerfilePath = path.join(config.baseDir, "docker", "sandbox.Dockerfile");
|
|
129
|
+
observers?.log?.("docker.building", {
|
|
130
|
+
image,
|
|
131
|
+
dockerfilePath,
|
|
132
|
+
});
|
|
133
|
+
recordSandboxLog("docker.building", { image, dockerfilePath });
|
|
134
|
+
|
|
135
|
+
await execDocker(["build", "-t", image, "-f", dockerfilePath, config.baseDir], {
|
|
136
|
+
timeout: 10 * 60 * 1000,
|
|
137
|
+
maxBuffer: 1024 * 1024,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
READY_IMAGES.add(image);
|
|
141
|
+
observers?.event?.("docker.ready", { image });
|
|
142
|
+
recordSandboxLog("docker.ready", { image });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function ensureDockerSandboxReady(config, observers, options = {}) {
|
|
146
|
+
const image = config.dockerSandboxImage;
|
|
147
|
+
if (!config.useDockerSandbox || (READY_IMAGES.has(image) && !options.forceBuild)) return;
|
|
148
|
+
|
|
149
|
+
const dockerfilePath = path.join(config.baseDir, "docker", "sandbox.Dockerfile");
|
|
150
|
+
await fs.access(dockerfilePath).catch(() => {
|
|
151
|
+
throw new Error(`Docker sandbox file is missing: ${dockerfilePath}`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!options.forceBuild && (await dockerImageExists(image))) return;
|
|
155
|
+
|
|
156
|
+
await buildDockerSandboxImage(config, observers);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
160
|
+
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
161
|
+
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
162
|
+
const userArgs = uid && gid ? ["--user", `${uid}:${gid}`] : [];
|
|
163
|
+
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
164
|
+
const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
|
|
165
|
+
const networkMode = policy.needsNetwork ? "bridge" : "none";
|
|
166
|
+
const readOnlyArgs =
|
|
167
|
+
mountMode === "ro"
|
|
168
|
+
? ["--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,size=128m"]
|
|
169
|
+
: ["--tmpfs", "/tmp:rw,nosuid,nodev,size=256m"];
|
|
170
|
+
|
|
171
|
+
return [
|
|
172
|
+
"run",
|
|
173
|
+
"--rm",
|
|
174
|
+
"--network",
|
|
175
|
+
networkMode,
|
|
176
|
+
"--cap-drop",
|
|
177
|
+
"ALL",
|
|
178
|
+
"--security-opt",
|
|
179
|
+
"no-new-privileges",
|
|
180
|
+
"--pids-limit",
|
|
181
|
+
"256",
|
|
182
|
+
"--memory",
|
|
183
|
+
"768m",
|
|
184
|
+
"--cpus",
|
|
185
|
+
"1.5",
|
|
186
|
+
...readOnlyArgs,
|
|
187
|
+
...userArgs,
|
|
188
|
+
"-e",
|
|
189
|
+
"HOME=/tmp",
|
|
190
|
+
"-e",
|
|
191
|
+
"NPM_CONFIG_USERCONFIG=/tmp/.npmrc",
|
|
192
|
+
"-e",
|
|
193
|
+
"PIP_DISABLE_PIP_VERSION_CHECK=1",
|
|
194
|
+
"-v",
|
|
195
|
+
`${config.commandCwd}:${DOCKER_WORKSPACE}:${mountMode}`,
|
|
196
|
+
"-w",
|
|
197
|
+
DOCKER_WORKSPACE,
|
|
198
|
+
config.dockerSandboxImage,
|
|
199
|
+
"bash",
|
|
200
|
+
"-lc",
|
|
201
|
+
String(command),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
206
|
+
const result = await execDocker(dockerRunArgs(command, config, policy), {
|
|
207
|
+
timeout: policy.needsNetwork ? 120000 : 15000,
|
|
208
|
+
maxBuffer: 300 * 1024,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const payload = {
|
|
212
|
+
stdout: result.stdout.trim().slice(0, 12000),
|
|
213
|
+
stderr: result.stderr.trim().slice(0, 6000),
|
|
214
|
+
};
|
|
215
|
+
recordSandboxLog("sandbox.command.completed", {
|
|
216
|
+
command,
|
|
217
|
+
category: policy.category,
|
|
218
|
+
sandboxMode: policy.sandboxMode,
|
|
219
|
+
network: policy.needsNetwork ? "bridge" : "none",
|
|
220
|
+
stdout: payload.stdout.slice(0, 1200),
|
|
221
|
+
stderr: payload.stderr.slice(0, 1200),
|
|
222
|
+
});
|
|
223
|
+
return payload;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function detectWorkspaceManifests(workspace) {
|
|
227
|
+
const candidates = ["package.json", "package-lock.json", "requirements.txt", "pyproject.toml", "environment.yml"];
|
|
228
|
+
const manifests = [];
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
if (await pathAccess(path.join(workspace, candidate), fsConstants.R_OK)) manifests.push(candidate);
|
|
231
|
+
}
|
|
232
|
+
return manifests;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function runDockerPreflight(config, options = {}) {
|
|
236
|
+
const buildImage = Boolean(options.buildImage);
|
|
237
|
+
const statusBefore = await getDockerSandboxStatus(config);
|
|
238
|
+
const manifests = await detectWorkspaceManifests(config.commandCwd);
|
|
239
|
+
let checks = [];
|
|
240
|
+
|
|
241
|
+
if (buildImage && statusBefore.dockerAvailable && statusBefore.dockerfileExists && !statusBefore.imageReady) {
|
|
242
|
+
await ensureDockerSandboxReady(config, {
|
|
243
|
+
log: (message, data) => recordSandboxLog(message, data),
|
|
244
|
+
event: (type, data) => recordSandboxLog(type, data),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let status = await getDockerSandboxStatus(config);
|
|
249
|
+
const runChecks = async () => {
|
|
250
|
+
const results = [];
|
|
251
|
+
for (const command of [
|
|
252
|
+
"node -v",
|
|
253
|
+
"npm -v",
|
|
254
|
+
"python3 --version",
|
|
255
|
+
"python3 -m pip --version",
|
|
256
|
+
"git --version",
|
|
257
|
+
"rg --version",
|
|
258
|
+
]) {
|
|
259
|
+
try {
|
|
260
|
+
const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }), {
|
|
261
|
+
timeout: 15000,
|
|
262
|
+
maxBuffer: 100 * 1024,
|
|
263
|
+
});
|
|
264
|
+
results.push({ command, ok: true, stdout: result.stdout.trim(), stderr: result.stderr.trim() });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
results.push({
|
|
267
|
+
command,
|
|
268
|
+
ok: false,
|
|
269
|
+
error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return results;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (status.imageReady) {
|
|
277
|
+
checks = await runChecks();
|
|
278
|
+
if (buildImage && checks.some((check) => !check.ok)) {
|
|
279
|
+
recordSandboxLog("docker.rebuilding-stale-image", {
|
|
280
|
+
image: config.dockerSandboxImage,
|
|
281
|
+
failedChecks: checks.filter((check) => !check.ok).map((check) => check.command),
|
|
282
|
+
});
|
|
283
|
+
await ensureDockerSandboxReady(
|
|
284
|
+
config,
|
|
285
|
+
{
|
|
286
|
+
log: (message, data) => recordSandboxLog(message, data),
|
|
287
|
+
event: (type, data) => recordSandboxLog(type, data),
|
|
288
|
+
},
|
|
289
|
+
{ forceBuild: true }
|
|
290
|
+
);
|
|
291
|
+
status = await getDockerSandboxStatus(config);
|
|
292
|
+
checks = await runChecks();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const result = {
|
|
297
|
+
ok: Boolean(status.dockerAvailable && status.workspaceReadable && status.dockerfileExists && status.imageReady),
|
|
298
|
+
status,
|
|
299
|
+
manifests,
|
|
300
|
+
checks,
|
|
301
|
+
logs: getSandboxLogs(),
|
|
302
|
+
};
|
|
303
|
+
recordSandboxLog("sandbox.preflight.completed", {
|
|
304
|
+
ok: result.ok,
|
|
305
|
+
manifests,
|
|
306
|
+
checks: checks.map((check) => ({ command: check.command, ok: check.ok })),
|
|
307
|
+
});
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { evaluateCommandPolicy } from "./command-policy.js";
|
|
2
|
+
|
|
3
|
+
const DESTRUCTIVE_KEYWORDS = [
|
|
4
|
+
"delete",
|
|
5
|
+
"remove",
|
|
6
|
+
"purchase",
|
|
7
|
+
"buy now",
|
|
8
|
+
"checkout",
|
|
9
|
+
"pay now",
|
|
10
|
+
"place order",
|
|
11
|
+
"confirm order",
|
|
12
|
+
"sign out",
|
|
13
|
+
"log out",
|
|
14
|
+
"logout",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const KNOWN_WRAPPERS = new Set(["codex", "claude", "gemini", "copilot", "qwen"]);
|
|
18
|
+
const DESTRUCTIVE_PROMPT_HINTS = [
|
|
19
|
+
"delete",
|
|
20
|
+
"remove files",
|
|
21
|
+
"rm -",
|
|
22
|
+
"git push",
|
|
23
|
+
"git reset",
|
|
24
|
+
"git checkout",
|
|
25
|
+
"install",
|
|
26
|
+
"sudo",
|
|
27
|
+
"deploy",
|
|
28
|
+
"publish",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function normalizeDomain(hostname) {
|
|
32
|
+
return hostname.replace(/^www\./, "").toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isDomainAllowed(urlString, allowedDomains) {
|
|
36
|
+
if (!allowedDomains || allowedDomains.length === 0) return true;
|
|
37
|
+
const url = new URL(urlString);
|
|
38
|
+
const hostname = normalizeDomain(url.hostname);
|
|
39
|
+
|
|
40
|
+
return allowedDomains.some((allowed) => {
|
|
41
|
+
const candidate = normalizeDomain(allowed);
|
|
42
|
+
return hostname === candidate || hostname.endsWith(`.${candidate}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
47
|
+
if (toolName === "open_url") {
|
|
48
|
+
if (!/^https?:\/\//.test(String(args.url || ""))) {
|
|
49
|
+
return { allowed: false, reason: "Only http and https URLs are allowed." };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isDomainAllowed(args.url, config.allowedDomains)) {
|
|
53
|
+
return {
|
|
54
|
+
allowed: false,
|
|
55
|
+
reason: `Domain is outside the allowlist: ${args.url}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (toolName === "click") {
|
|
63
|
+
const element = snapshot.elements.find((item) => item.id === String(args.id));
|
|
64
|
+
if (!element) return { allowed: false, reason: `Element ${args.id} is not in the latest snapshot.` };
|
|
65
|
+
|
|
66
|
+
const label = `${element.text} ${element.ariaLabel}`.toLowerCase();
|
|
67
|
+
if (!config.allowDestructive && DESTRUCTIVE_KEYWORDS.some((word) => label.includes(word))) {
|
|
68
|
+
return { allowed: false, reason: `Blocked potentially destructive click target: "${label.trim()}"` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { allowed: true, element };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (toolName === "type") {
|
|
75
|
+
const element = snapshot.elements.find((item) => item.id === String(args.id));
|
|
76
|
+
if (!element) return { allowed: false, reason: `Element ${args.id} is not in the latest snapshot.` };
|
|
77
|
+
|
|
78
|
+
const looksSensitive =
|
|
79
|
+
element.inputType === "password" ||
|
|
80
|
+
/password/.test(element.autocomplete || "") ||
|
|
81
|
+
/password/.test(`${element.text} ${element.ariaLabel} ${element.placeholder}`.toLowerCase());
|
|
82
|
+
|
|
83
|
+
if (!config.allowPasswords && looksSensitive) {
|
|
84
|
+
return { allowed: false, reason: "Typing into password-like fields is blocked by default." };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { allowed: true, element };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (toolName === "run_command") {
|
|
91
|
+
const command = String(args.command || "").trim();
|
|
92
|
+
return evaluateCommandPolicy(command, config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (toolName === "delegate_agent") {
|
|
96
|
+
if (!config.allowWrapperTools) {
|
|
97
|
+
return { allowed: false, reason: "Agent wrapper tools are disabled for this run." };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const wrapper = String(args.wrapper || "");
|
|
101
|
+
if (!KNOWN_WRAPPERS.has(wrapper)) {
|
|
102
|
+
return { allowed: false, reason: `Unknown agent wrapper: ${wrapper}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const prompt = String(args.prompt || "").trim();
|
|
106
|
+
if (prompt.length < 8) {
|
|
107
|
+
return { allowed: false, reason: "Agent wrapper prompt is too short." };
|
|
108
|
+
}
|
|
109
|
+
if (prompt.length > 4000) {
|
|
110
|
+
return { allowed: false, reason: "Agent wrapper prompt is too long." };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const loweredPrompt = prompt.toLowerCase();
|
|
114
|
+
if (!config.allowDestructive && DESTRUCTIVE_PROMPT_HINTS.some((hint) => loweredPrompt.includes(hint))) {
|
|
115
|
+
return { allowed: false, reason: "Agent wrapper prompt appears to request write-capable or destructive work." };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { allowed: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { allowed: true };
|
|
122
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { WRAPPER_NAMES, wrapperStatusText } from "./tool-wrappers.js";
|
|
3
|
+
|
|
4
|
+
export function createClient(config) {
|
|
5
|
+
return new OpenAI({
|
|
6
|
+
apiKey: config.apiKey,
|
|
7
|
+
baseURL: config.baseURL,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function createPlan(client, config, state) {
|
|
12
|
+
const response = await client.chat.completions.create({
|
|
13
|
+
model: config.model,
|
|
14
|
+
temperature: 0,
|
|
15
|
+
messages: [
|
|
16
|
+
{
|
|
17
|
+
role: "system",
|
|
18
|
+
content:
|
|
19
|
+
"You are planning a browser-and-shell automation task. If a local shell command can satisfy the goal, prefer that before browser actions. Treat any suggested start URL as optional. Write a concise execution plan with 3 to 6 steps. Mention risks or blockers when relevant. Keep it short and practical.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
role: "user",
|
|
23
|
+
content: [
|
|
24
|
+
`Goal: ${state.goal}`,
|
|
25
|
+
state.startUrl ? `Suggested start URL: ${state.startUrl}` : "",
|
|
26
|
+
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
27
|
+
config.allowShellTool
|
|
28
|
+
? `Shell tool is enabled in ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow.`
|
|
29
|
+
: "",
|
|
30
|
+
config.allowWrapperTools ? `Agent wrappers are enabled: ${wrapperStatusText()}.` : "",
|
|
31
|
+
"Return a numbered plan only.",
|
|
32
|
+
]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join("\n"),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return response.choices[0]?.message?.content?.trim() || "1. Inspect the page.\n2. Use the smallest safe action.\n3. Finish with a concise answer.";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function requestNextStep(client, config, messages) {
|
|
43
|
+
const tools = [
|
|
44
|
+
{
|
|
45
|
+
type: "function",
|
|
46
|
+
function: {
|
|
47
|
+
name: "open_url",
|
|
48
|
+
description: "Open an absolute http or https URL in the browser.",
|
|
49
|
+
parameters: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
url: { type: "string" },
|
|
53
|
+
},
|
|
54
|
+
required: ["url"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "function",
|
|
61
|
+
function: {
|
|
62
|
+
name: "click",
|
|
63
|
+
description: "Click a visible element by its id from the latest snapshot.",
|
|
64
|
+
parameters: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
id: { type: "string" },
|
|
68
|
+
},
|
|
69
|
+
required: ["id"],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: "function",
|
|
76
|
+
function: {
|
|
77
|
+
name: "type",
|
|
78
|
+
description: "Type text into an input-like element by id. Optionally press Enter after typing.",
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
id: { type: "string" },
|
|
83
|
+
text: { type: "string" },
|
|
84
|
+
pressEnter: { type: "boolean" },
|
|
85
|
+
},
|
|
86
|
+
required: ["id", "text"],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "function",
|
|
93
|
+
function: {
|
|
94
|
+
name: "scroll",
|
|
95
|
+
description: "Scroll the page vertically.",
|
|
96
|
+
parameters: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
direction: { type: "string", enum: ["up", "down"] },
|
|
100
|
+
amount: { type: "integer" },
|
|
101
|
+
},
|
|
102
|
+
required: ["direction"],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "function",
|
|
109
|
+
function: {
|
|
110
|
+
name: "press",
|
|
111
|
+
description: "Press a keyboard key such as Enter, Tab, Escape, ArrowDown, or ArrowUp.",
|
|
112
|
+
parameters: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
key: { type: "string" },
|
|
116
|
+
},
|
|
117
|
+
required: ["key"],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: "function",
|
|
124
|
+
function: {
|
|
125
|
+
name: "back",
|
|
126
|
+
description: "Go back to the previous page in browser history.",
|
|
127
|
+
parameters: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {},
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: "function",
|
|
136
|
+
function: {
|
|
137
|
+
name: "wait",
|
|
138
|
+
description: "Wait for the page to update after an action.",
|
|
139
|
+
parameters: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
ms: { type: "integer" },
|
|
143
|
+
},
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "function",
|
|
150
|
+
function: {
|
|
151
|
+
name: "finish",
|
|
152
|
+
description: "Return the final answer when the goal is complete.",
|
|
153
|
+
parameters: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
result: { type: "string" },
|
|
157
|
+
},
|
|
158
|
+
required: ["result"],
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
if (config.allowShellTool) {
|
|
166
|
+
tools.splice(-1, 0, {
|
|
167
|
+
type: "function",
|
|
168
|
+
function: {
|
|
169
|
+
name: "run_command",
|
|
170
|
+
description:
|
|
171
|
+
"Run an allowlisted terminal command in the configured working directory. Good for inspection, tests, and approved Docker package/environment setup. NPM publishing, tokens, sudo, arbitrary network commands, and destructive git/file actions are blocked.",
|
|
172
|
+
parameters: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
command: { type: "string" },
|
|
176
|
+
},
|
|
177
|
+
required: ["command"],
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (config.allowWrapperTools) {
|
|
185
|
+
tools.splice(-1, 0, {
|
|
186
|
+
type: "function",
|
|
187
|
+
function: {
|
|
188
|
+
name: "delegate_agent",
|
|
189
|
+
description:
|
|
190
|
+
"Ask an installed external coding agent wrapper for advisory help. Use for codebase analysis, implementation strategy, or second-opinion review. The wrapper is instructed to avoid modifying files.",
|
|
191
|
+
parameters: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
wrapper: { type: "string", enum: WRAPPER_NAMES },
|
|
195
|
+
prompt: { type: "string" },
|
|
196
|
+
},
|
|
197
|
+
required: ["wrapper", "prompt"],
|
|
198
|
+
additionalProperties: false,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return client.chat.completions.create({
|
|
205
|
+
model: config.model,
|
|
206
|
+
temperature: 0,
|
|
207
|
+
tool_choice: "auto",
|
|
208
|
+
parallel_tool_calls: false,
|
|
209
|
+
messages,
|
|
210
|
+
tools,
|
|
211
|
+
});
|
|
212
|
+
}
|