@lazyingart/agintiflow 0.1.0 → 0.2.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 +6 -3
- package/README.md +68 -15
- package/docker/sandbox.Dockerfile +8 -0
- package/package.json +11 -3
- package/public/app.js +644 -11
- package/public/index.html +165 -20
- package/public/styles.css +586 -13
- package/scripts/setup-agent-toolchain-docker.sh +51 -0
- package/scripts/smoke-coding-tools.js +138 -0
- package/scripts/smoke-toolchain-docker.js +185 -0
- package/scripts/smoke-web-api.js +217 -0
- package/src/agent-runner.js +286 -21
- package/src/artifact-tunnel.js +401 -0
- package/src/cli.js +47 -3
- package/src/command-policy.js +21 -0
- package/src/config.js +9 -2
- package/src/docker-sandbox.js +38 -8
- package/src/guardrails.js +31 -0
- package/src/model-client.js +300 -6
- package/src/model-routing.js +40 -0
- package/src/session-store.js +4 -0
- package/src/tool-wrappers.js +6 -0
- package/src/web-db.js +48 -5
- package/src/workspace-tools.js +438 -0
- package/web.js +265 -14
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { redactSensitiveText } from "./redaction.js";
|
|
5
|
+
import { checkWorkspaceToolUse, resolveWorkspacePath } from "./workspace-tools.js";
|
|
6
|
+
|
|
7
|
+
const MAX_INLINE_TEXT_BYTES = 120_000;
|
|
8
|
+
const MAX_ARTIFACT_TEXT_BYTES = 520_000;
|
|
9
|
+
const MAX_ARTIFACT_IMAGE_BYTES = 4_000_000;
|
|
10
|
+
const IMAGE_MIME_BY_EXT = new Map([
|
|
11
|
+
[".png", "image/png"],
|
|
12
|
+
[".jpg", "image/jpeg"],
|
|
13
|
+
[".jpeg", "image/jpeg"],
|
|
14
|
+
[".gif", "image/gif"],
|
|
15
|
+
[".webp", "image/webp"],
|
|
16
|
+
[".svg", "image/svg+xml"],
|
|
17
|
+
]);
|
|
18
|
+
const BINARY_RENDER_MIME_BY_EXT = new Map([[".pdf", "application/pdf"]]);
|
|
19
|
+
|
|
20
|
+
function normalizeDisplayPath(value) {
|
|
21
|
+
return String(value || "")
|
|
22
|
+
.split(path.sep)
|
|
23
|
+
.join("/");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isInside(root, target) {
|
|
27
|
+
const relative = path.relative(root, target);
|
|
28
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stableId(...parts) {
|
|
32
|
+
return crypto.createHash("sha256").update(parts.map((part) => String(part ?? "")).join("\0")).digest("base64url");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function previewText(value, limit = 220) {
|
|
36
|
+
const text = redactSensitiveText(String(value || "")).replace(/\s+/g, " ").trim();
|
|
37
|
+
return text.length > limit ? `${text.slice(0, limit).trim()}...` : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeKind(kind, filePath = "") {
|
|
41
|
+
const candidate = String(kind || "").toLowerCase();
|
|
42
|
+
if (candidate === "file") {
|
|
43
|
+
if (IMAGE_MIME_BY_EXT.has(path.extname(String(filePath || "")).toLowerCase())) return "image";
|
|
44
|
+
if (BINARY_RENDER_MIME_BY_EXT.has(path.extname(String(filePath || "")).toLowerCase())) return "pdf";
|
|
45
|
+
if (String(filePath || "").toLowerCase().endsWith(".json")) return "json";
|
|
46
|
+
if (String(filePath || "").toLowerCase().endsWith(".md")) return "markdown";
|
|
47
|
+
return "file";
|
|
48
|
+
}
|
|
49
|
+
if (["image", "markdown", "text", "json", "diff", "pdf"].includes(candidate)) return candidate;
|
|
50
|
+
if (IMAGE_MIME_BY_EXT.has(path.extname(String(filePath || "")).toLowerCase())) return "image";
|
|
51
|
+
if (BINARY_RENDER_MIME_BY_EXT.has(path.extname(String(filePath || "")).toLowerCase())) return "pdf";
|
|
52
|
+
if (String(filePath || "").toLowerCase().endsWith(".json")) return "json";
|
|
53
|
+
if (String(filePath || "").toLowerCase().endsWith(".md")) return "markdown";
|
|
54
|
+
return "text";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mimeForPath(filePath) {
|
|
58
|
+
const ext = path.extname(String(filePath || "")).toLowerCase();
|
|
59
|
+
return IMAGE_MIME_BY_EXT.get(ext) || BINARY_RENDER_MIME_BY_EXT.get(ext) || "text/plain; charset=utf-8";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function publicArtifact(item) {
|
|
63
|
+
return {
|
|
64
|
+
id: item.id,
|
|
65
|
+
sessionId: item.sessionId,
|
|
66
|
+
kind: item.kind,
|
|
67
|
+
title: item.title,
|
|
68
|
+
path: item.path || "",
|
|
69
|
+
preview: item.preview || "",
|
|
70
|
+
source: item.source,
|
|
71
|
+
tab: item.tab,
|
|
72
|
+
createdAt: item.createdAt,
|
|
73
|
+
selected: Boolean(item.selected),
|
|
74
|
+
mime: item.mime || "",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sessionRelativePath(store, filePath) {
|
|
79
|
+
const absolutePath = path.resolve(String(filePath || ""));
|
|
80
|
+
if (!isInside(store.sessionDir, absolutePath)) return "";
|
|
81
|
+
return normalizeDisplayPath(path.relative(store.sessionDir, absolutePath));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addSnapshotArtifacts(items, event, store, sessionId) {
|
|
85
|
+
const data = event.data || {};
|
|
86
|
+
if (data.screenshotPath) {
|
|
87
|
+
const displayPath = sessionRelativePath(store, data.screenshotPath);
|
|
88
|
+
if (displayPath) {
|
|
89
|
+
items.push({
|
|
90
|
+
id: stableId(sessionId, event.timestamp, "snapshot-image", displayPath),
|
|
91
|
+
sessionId,
|
|
92
|
+
kind: "image",
|
|
93
|
+
title: `Browser screenshot step ${data.step || "?"}`,
|
|
94
|
+
path: displayPath,
|
|
95
|
+
preview: data.title || data.url || "Captured browser screenshot.",
|
|
96
|
+
source: "snapshot",
|
|
97
|
+
tab: "canvas",
|
|
98
|
+
createdAt: event.timestamp,
|
|
99
|
+
mime: "image/png",
|
|
100
|
+
ref: {
|
|
101
|
+
type: "session-file",
|
|
102
|
+
path: path.resolve(data.screenshotPath),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (data.snapshotPath) {
|
|
109
|
+
const displayPath = sessionRelativePath(store, data.snapshotPath);
|
|
110
|
+
if (displayPath) {
|
|
111
|
+
items.push({
|
|
112
|
+
id: stableId(sessionId, event.timestamp, "snapshot-json", displayPath),
|
|
113
|
+
sessionId,
|
|
114
|
+
kind: "json",
|
|
115
|
+
title: `Runtime snapshot step ${data.step || "?"}`,
|
|
116
|
+
path: displayPath,
|
|
117
|
+
preview: data.title || data.url || "Browser/runtime snapshot JSON.",
|
|
118
|
+
source: "snapshot",
|
|
119
|
+
tab: "explorer",
|
|
120
|
+
createdAt: event.timestamp,
|
|
121
|
+
mime: "application/json",
|
|
122
|
+
ref: {
|
|
123
|
+
type: "session-file",
|
|
124
|
+
path: path.resolve(data.snapshotPath),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function addWorkspaceArtifacts(items, event, sessionId) {
|
|
132
|
+
const data = event.data || {};
|
|
133
|
+
const filePath = normalizeDisplayPath(data.path || "");
|
|
134
|
+
if (!filePath) return;
|
|
135
|
+
|
|
136
|
+
const diff = redactSensitiveText(String(data.diff || ""));
|
|
137
|
+
items.push({
|
|
138
|
+
id: stableId(sessionId, event.timestamp, "file-diff", filePath, diff),
|
|
139
|
+
sessionId,
|
|
140
|
+
kind: "diff",
|
|
141
|
+
title: `Change: ${filePath}`,
|
|
142
|
+
path: filePath,
|
|
143
|
+
preview: previewText(diff || `${data.toolName || "file tool"} changed ${filePath}`),
|
|
144
|
+
source: "file-change",
|
|
145
|
+
tab: "notifications",
|
|
146
|
+
createdAt: event.timestamp,
|
|
147
|
+
mime: "text/x-diff",
|
|
148
|
+
ref: {
|
|
149
|
+
type: "inline",
|
|
150
|
+
text: diff || JSON.stringify(data, null, 2),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
items.push({
|
|
155
|
+
id: stableId(sessionId, event.timestamp, "workspace-file", filePath),
|
|
156
|
+
sessionId,
|
|
157
|
+
kind: normalizeKind("file", filePath),
|
|
158
|
+
title: `Workspace file: ${filePath}`,
|
|
159
|
+
path: filePath,
|
|
160
|
+
preview: "Open the current workspace file through guarded read policy.",
|
|
161
|
+
source: "workspace-file",
|
|
162
|
+
tab: "explorer",
|
|
163
|
+
createdAt: event.timestamp,
|
|
164
|
+
mime: mimeForPath(filePath),
|
|
165
|
+
ref: {
|
|
166
|
+
type: "workspace-file",
|
|
167
|
+
path: filePath,
|
|
168
|
+
commandCwd: data.commandCwd || "",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function addCanvasArtifact(items, event, sessionId) {
|
|
174
|
+
const data = event.data || {};
|
|
175
|
+
const artifactId = data.artifactId || stableId(sessionId, event.timestamp, "canvas", data.title, data.path);
|
|
176
|
+
const artifactPath = normalizeDisplayPath(data.path || "");
|
|
177
|
+
const text = typeof data.content === "string" ? redactSensitiveText(data.content) : "";
|
|
178
|
+
items.push({
|
|
179
|
+
id: artifactId,
|
|
180
|
+
sessionId,
|
|
181
|
+
kind: normalizeKind(data.kind, artifactPath),
|
|
182
|
+
title: String(data.title || "Agent canvas item").slice(0, 120),
|
|
183
|
+
path: artifactPath,
|
|
184
|
+
preview: previewText(data.note || text || artifactPath || "Agent selected this item for canvas display."),
|
|
185
|
+
source: "agent-canvas",
|
|
186
|
+
tab: "canvas",
|
|
187
|
+
createdAt: event.timestamp,
|
|
188
|
+
mime: mimeForPath(artifactPath),
|
|
189
|
+
ref: text
|
|
190
|
+
? {
|
|
191
|
+
type: "inline",
|
|
192
|
+
text,
|
|
193
|
+
}
|
|
194
|
+
: artifactPath
|
|
195
|
+
? {
|
|
196
|
+
type: "workspace-file",
|
|
197
|
+
path: artifactPath,
|
|
198
|
+
commandCwd: data.commandCwd || "",
|
|
199
|
+
}
|
|
200
|
+
: {
|
|
201
|
+
type: "inline",
|
|
202
|
+
text: redactSensitiveText(String(data.note || "")),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function addFinalAnswerArtifact(items, event, sessionId) {
|
|
208
|
+
const result = String(event.data?.result || "");
|
|
209
|
+
if (!result.trim()) return;
|
|
210
|
+
items.push({
|
|
211
|
+
id: stableId(sessionId, event.timestamp, "final-answer", result),
|
|
212
|
+
sessionId,
|
|
213
|
+
kind: "markdown",
|
|
214
|
+
title: "Final answer",
|
|
215
|
+
path: "",
|
|
216
|
+
preview: previewText(result),
|
|
217
|
+
source: "session",
|
|
218
|
+
tab: "notifications",
|
|
219
|
+
createdAt: event.timestamp,
|
|
220
|
+
mime: "text/markdown; charset=utf-8",
|
|
221
|
+
ref: {
|
|
222
|
+
type: "inline",
|
|
223
|
+
text: redactSensitiveText(result),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function selectedArtifactId(events) {
|
|
229
|
+
return [...events].reverse().find((event) => event.type === "canvas.selected")?.data?.artifactId || "";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildArtifacts({ sessionId, events, store }) {
|
|
233
|
+
const items = [];
|
|
234
|
+
const selectedId = selectedArtifactId(events);
|
|
235
|
+
|
|
236
|
+
for (const event of events) {
|
|
237
|
+
if (event.type === "canvas.item") addCanvasArtifact(items, event, sessionId);
|
|
238
|
+
if (event.type === "snapshot.captured") addSnapshotArtifacts(items, event, store, sessionId);
|
|
239
|
+
if (event.type === "file.changed") addWorkspaceArtifacts(items, event, sessionId);
|
|
240
|
+
if (event.type === "session.finished") addFinalAnswerArtifact(items, event, sessionId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
items.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
|
|
244
|
+
const fallbackSelected = selectedId || items.find((item) => item.tab === "canvas")?.id || items[0]?.id || "";
|
|
245
|
+
for (const item of items) item.selected = item.id === fallbackSelected;
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
items,
|
|
249
|
+
selectedItemId: fallbackSelected,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function serializeArtifacts(items) {
|
|
254
|
+
return items.map(publicArtifact);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function countUnreadArtifacts(items, seenAfter = "") {
|
|
258
|
+
const seenAt = Date.parse(String(seenAfter || ""));
|
|
259
|
+
if (!Number.isFinite(seenAt)) return items.length;
|
|
260
|
+
return items.filter((item) => Date.parse(item.createdAt) > seenAt).length;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function findArtifact(items, artifactId) {
|
|
264
|
+
return items.find((item) => item.id === artifactId) || null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function readArtifactContent(item, { store, config }) {
|
|
268
|
+
if (!item) {
|
|
269
|
+
return { ok: false, error: "Artifact not found." };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (item.ref?.type === "inline") {
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
id: item.id,
|
|
276
|
+
kind: item.kind,
|
|
277
|
+
title: item.title,
|
|
278
|
+
path: item.path || "",
|
|
279
|
+
mime: item.mime || "text/plain; charset=utf-8",
|
|
280
|
+
text: redactSensitiveText(String(item.ref.text || "")),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (item.ref?.type === "session-file") {
|
|
285
|
+
const absolutePath = path.resolve(item.ref.path);
|
|
286
|
+
if (!isInside(store.sessionDir, absolutePath)) {
|
|
287
|
+
return { ok: false, error: "Artifact path is outside this session." };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const stat = await fs.stat(absolutePath).catch(() => null);
|
|
291
|
+
if (!stat?.isFile()) return { ok: false, error: "Artifact file is missing." };
|
|
292
|
+
const mime = mimeForPath(absolutePath);
|
|
293
|
+
const isBinaryRenderable = mime.startsWith("image/") || mime === "application/pdf";
|
|
294
|
+
const maxBytes = isBinaryRenderable ? MAX_ARTIFACT_IMAGE_BYTES : MAX_ARTIFACT_TEXT_BYTES;
|
|
295
|
+
if (stat.size > maxBytes) return { ok: false, error: "Artifact is too large to preview safely." };
|
|
296
|
+
|
|
297
|
+
const buffer = await fs.readFile(absolutePath);
|
|
298
|
+
if (isBinaryRenderable) {
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
id: item.id,
|
|
302
|
+
kind: mime === "application/pdf" ? "pdf" : "image",
|
|
303
|
+
title: item.title,
|
|
304
|
+
path: item.path || "",
|
|
305
|
+
mime,
|
|
306
|
+
dataUrl: `data:${mime};base64,${buffer.toString("base64")}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (buffer.includes(0)) return { ok: false, error: "Binary artifact cannot be rendered as text." };
|
|
311
|
+
return {
|
|
312
|
+
ok: true,
|
|
313
|
+
id: item.id,
|
|
314
|
+
kind: item.kind,
|
|
315
|
+
title: item.title,
|
|
316
|
+
path: item.path || "",
|
|
317
|
+
mime,
|
|
318
|
+
text: redactSensitiveText(buffer.toString("utf8")),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (item.ref?.type === "workspace-file") {
|
|
323
|
+
const itemConfig = item.ref.commandCwd ? { ...config, commandCwd: item.ref.commandCwd } : config;
|
|
324
|
+
const guard = checkWorkspaceToolUse("read_file", { path: item.ref.path }, itemConfig);
|
|
325
|
+
if (!guard.allowed) {
|
|
326
|
+
return { ok: false, error: guard.reason || "Workspace read blocked." };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const target = resolveWorkspacePath(itemConfig, item.ref.path);
|
|
330
|
+
const stat = await fs.stat(target.absolutePath).catch(() => null);
|
|
331
|
+
if (!stat?.isFile()) return { ok: false, error: "Workspace file is missing." };
|
|
332
|
+
|
|
333
|
+
const mime = mimeForPath(target.absolutePath);
|
|
334
|
+
const isBinaryRenderable = mime.startsWith("image/") || mime === "application/pdf";
|
|
335
|
+
const maxBytes = isBinaryRenderable ? MAX_ARTIFACT_IMAGE_BYTES : MAX_ARTIFACT_TEXT_BYTES;
|
|
336
|
+
if (stat.size > maxBytes) return { ok: false, error: "Workspace file is too large to preview safely." };
|
|
337
|
+
|
|
338
|
+
const buffer = await fs.readFile(target.absolutePath);
|
|
339
|
+
if (isBinaryRenderable) {
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
id: item.id,
|
|
343
|
+
kind: mime === "application/pdf" ? "pdf" : "image",
|
|
344
|
+
title: item.title,
|
|
345
|
+
path: item.path || "",
|
|
346
|
+
mime,
|
|
347
|
+
dataUrl: `data:${mime};base64,${buffer.toString("base64")}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (buffer.includes(0)) return { ok: false, error: "Binary workspace file cannot be rendered as text." };
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
id: item.id,
|
|
355
|
+
kind: item.kind,
|
|
356
|
+
title: item.title,
|
|
357
|
+
path: item.path || "",
|
|
358
|
+
mime,
|
|
359
|
+
text: redactSensitiveText(buffer.toString("utf8")),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { ok: false, error: "Artifact has no readable content." };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function normalizeCanvasPayload(args, config) {
|
|
367
|
+
const artifactId = `canvas-${crypto.randomUUID()}`;
|
|
368
|
+
const title = String(args.title || "Agent canvas item").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
369
|
+
const pathInput = String(args.path || "").trim();
|
|
370
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
371
|
+
const note = typeof args.note === "string" ? args.note : "";
|
|
372
|
+
|
|
373
|
+
if (!title) {
|
|
374
|
+
return { ok: false, reason: "Canvas title is required." };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (Buffer.byteLength(content, "utf8") > MAX_INLINE_TEXT_BYTES) {
|
|
378
|
+
return { ok: false, reason: "Canvas content is too large. Write it to a workspace file and send the path instead." };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let relativePath = "";
|
|
382
|
+
if (pathInput) {
|
|
383
|
+
const guard = checkWorkspaceToolUse("read_file", { path: pathInput }, config);
|
|
384
|
+
if (!guard.allowed) return { ok: false, reason: guard.reason || "Canvas path is blocked." };
|
|
385
|
+
relativePath = resolveWorkspacePath(config, pathInput).relativePath;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
payload: {
|
|
391
|
+
artifactId,
|
|
392
|
+
title,
|
|
393
|
+
kind: normalizeKind(args.kind, relativePath),
|
|
394
|
+
path: relativePath,
|
|
395
|
+
content: content ? redactSensitiveText(content) : "",
|
|
396
|
+
contentBytes: content ? Buffer.byteLength(content, "utf8") : 0,
|
|
397
|
+
note: note ? previewText(note, 500) : "",
|
|
398
|
+
selected: args.selected !== false,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,10 @@ import { loadConfig } from "./config.js";
|
|
|
3
3
|
import { listAgentWrappers } from "./tool-wrappers.js";
|
|
4
4
|
import { getModelPresets } from "./model-routing.js";
|
|
5
5
|
import { getDockerSandboxStatus, runDockerPreflight } from "./docker-sandbox.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
10
|
|
|
7
11
|
function readOption(argv, index) {
|
|
8
12
|
const value = argv[index + 1];
|
|
@@ -23,7 +27,9 @@ export function parseArgs(argv) {
|
|
|
23
27
|
sandboxMode: "",
|
|
24
28
|
packageInstallPolicy: "",
|
|
25
29
|
allowShellTool: undefined,
|
|
30
|
+
allowFileTools: undefined,
|
|
26
31
|
allowWrapperTools: undefined,
|
|
32
|
+
preferredWrapper: "",
|
|
27
33
|
useDockerSandbox: undefined,
|
|
28
34
|
headless: undefined,
|
|
29
35
|
maxSteps: undefined,
|
|
@@ -31,11 +37,28 @@ export function parseArgs(argv) {
|
|
|
31
37
|
listWrappers: false,
|
|
32
38
|
sandboxStatus: false,
|
|
33
39
|
sandboxPreflight: false,
|
|
40
|
+
web: false,
|
|
41
|
+
port: "",
|
|
42
|
+
host: "",
|
|
34
43
|
};
|
|
35
44
|
|
|
36
45
|
const parts = [];
|
|
37
46
|
for (let i = 0; i < argv.length; i += 1) {
|
|
38
47
|
const arg = argv[i];
|
|
48
|
+
if (arg === "web" || arg === "--web") {
|
|
49
|
+
result.web = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === "--port") {
|
|
53
|
+
result.port = readOption(argv, i);
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg === "--host") {
|
|
58
|
+
result.host = readOption(argv, i);
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
39
62
|
if (arg === "--start-url") {
|
|
40
63
|
result.startUrl = readOption(argv, i);
|
|
41
64
|
i += 1;
|
|
@@ -95,10 +118,23 @@ export function parseArgs(argv) {
|
|
|
95
118
|
result.allowShellTool = true;
|
|
96
119
|
continue;
|
|
97
120
|
}
|
|
121
|
+
if (arg === "--allow-file-tools") {
|
|
122
|
+
result.allowFileTools = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (arg === "--no-file-tools") {
|
|
126
|
+
result.allowFileTools = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
98
129
|
if (arg === "--allow-wrappers") {
|
|
99
130
|
result.allowWrapperTools = true;
|
|
100
131
|
continue;
|
|
101
132
|
}
|
|
133
|
+
if (arg === "--wrapper" || arg === "--preferred-wrapper") {
|
|
134
|
+
result.preferredWrapper = readOption(argv, i);
|
|
135
|
+
i += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
102
138
|
if (arg === "--docker-sandbox") {
|
|
103
139
|
result.useDockerSandbox = true;
|
|
104
140
|
result.sandboxMode = result.sandboxMode || "docker-readonly";
|
|
@@ -150,6 +186,14 @@ function printWrappers() {
|
|
|
150
186
|
export async function main(argv = process.argv.slice(2)) {
|
|
151
187
|
const args = parseArgs(argv);
|
|
152
188
|
|
|
189
|
+
if (args.web) {
|
|
190
|
+
if (args.port) process.env.PORT = String(args.port);
|
|
191
|
+
if (args.host) process.env.HOST = String(args.host);
|
|
192
|
+
process.env.AGINTIFLOW_PACKAGE_DIR = packageDir;
|
|
193
|
+
await import("../web.js");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
153
197
|
if (args.listRoutes) {
|
|
154
198
|
printRoutes();
|
|
155
199
|
return;
|
|
@@ -161,7 +205,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
161
205
|
}
|
|
162
206
|
|
|
163
207
|
if (args.sandboxStatus || args.sandboxPreflight) {
|
|
164
|
-
const config = loadConfig({ ...args, goal: args.goal || "sandbox preflight" });
|
|
208
|
+
const config = loadConfig({ ...args, goal: args.goal || "sandbox preflight" }, { packageDir });
|
|
165
209
|
const result = args.sandboxPreflight
|
|
166
210
|
? await runDockerPreflight(config, { buildImage: true })
|
|
167
211
|
: { ok: true, status: await getDockerSandboxStatus(config) };
|
|
@@ -171,11 +215,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
171
215
|
|
|
172
216
|
if (!args.goal && !args.resume) {
|
|
173
217
|
console.error(
|
|
174
|
-
'Usage: aginti-cli [--routing smart|fast|complex|manual] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--allow-shell] [--allow-wrappers] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
218
|
+
'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--allow-shell] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
175
219
|
);
|
|
176
220
|
process.exit(1);
|
|
177
221
|
}
|
|
178
222
|
|
|
179
|
-
const config = loadConfig(args);
|
|
223
|
+
const config = loadConfig(args, { packageDir });
|
|
180
224
|
await runAgent(config);
|
|
181
225
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -5,6 +5,8 @@ const READ_ONLY_PATTERNS = [
|
|
|
5
5
|
/^pwd$/,
|
|
6
6
|
/^date$/,
|
|
7
7
|
/^whoami$/,
|
|
8
|
+
/^which\s+[-\w.]+(?:\s+[-\w.]+)*$/,
|
|
9
|
+
/^command\s+-v\s+[-\w.]+$/,
|
|
8
10
|
/^uname(?:\s+-a)?$/,
|
|
9
11
|
/^ls(?:\s+[-\w./~*]+)*$/,
|
|
10
12
|
/^find(?:\s+[./~\w-]+)*(?:\s+-maxdepth\s+\d+)?(?:\s+-type\s+[fd])?$/,
|
|
@@ -31,6 +33,12 @@ const TEST_PATTERNS = [
|
|
|
31
33
|
/^pytest(?:\s+[-\w./:=]+)*$/,
|
|
32
34
|
];
|
|
33
35
|
|
|
36
|
+
const TOOLCHAIN_PATTERNS = [
|
|
37
|
+
/^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
|
|
38
|
+
/^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
|
|
39
|
+
/^pdflatex\s+(?:(?:-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+|-jobname\s+[-\w./]+)\s+)*[-\w./]+\.tex$/,
|
|
40
|
+
];
|
|
41
|
+
|
|
34
42
|
const PACKAGE_INSTALL_PATTERNS = [
|
|
35
43
|
/^npm\s+ci$/,
|
|
36
44
|
/^npm\s+install$/,
|
|
@@ -121,6 +129,9 @@ export function classifyCommand(command) {
|
|
|
121
129
|
if (matchAny(TEST_PATTERNS, normalized)) {
|
|
122
130
|
return { category: "test", needsNetwork: false, writesWorkspace: false };
|
|
123
131
|
}
|
|
132
|
+
if (matchAny(TOOLCHAIN_PATTERNS, normalized)) {
|
|
133
|
+
return { category: "toolchain", needsNetwork: false, writesWorkspace: true };
|
|
134
|
+
}
|
|
124
135
|
if (matchAny(PACKAGE_INSTALL_PATTERNS, normalized)) {
|
|
125
136
|
return { category: "package-install", needsNetwork: true, writesWorkspace: true };
|
|
126
137
|
}
|
|
@@ -176,6 +187,16 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
176
187
|
}
|
|
177
188
|
}
|
|
178
189
|
|
|
190
|
+
if (classification.writesWorkspace && config.useDockerSandbox && sandboxMode !== "docker-workspace") {
|
|
191
|
+
return {
|
|
192
|
+
allowed: false,
|
|
193
|
+
category: classification.category,
|
|
194
|
+
reason: "Workspace-writing toolchain commands must run in Docker workspace-write sandbox mode.",
|
|
195
|
+
sandboxMode,
|
|
196
|
+
packageInstallPolicy,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
return {
|
|
180
201
|
allowed: true,
|
|
181
202
|
...classification,
|
package/src/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { getProviderDefaults, normalizeRoutingMode, selectModelRoute } from "./model-routing.js";
|
|
4
4
|
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
|
|
5
|
+
import { normalizeWrapperName } from "./tool-wrappers.js";
|
|
5
6
|
|
|
6
7
|
function parseBoolean(value, fallback) {
|
|
7
8
|
if (value === undefined) return fallback;
|
|
@@ -37,6 +38,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
37
38
|
|
|
38
39
|
const defaults = getProviderDefaults(route.provider);
|
|
39
40
|
const baseDir = path.resolve(overrides.baseDir || process.cwd());
|
|
41
|
+
const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
|
|
40
42
|
const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, false);
|
|
41
43
|
const requestedSandboxMode =
|
|
42
44
|
overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-readonly" : "host");
|
|
@@ -45,6 +47,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
45
47
|
return {
|
|
46
48
|
...defaults,
|
|
47
49
|
baseDir,
|
|
50
|
+
packageDir,
|
|
48
51
|
goal: args.goal || "",
|
|
49
52
|
startUrl: args.startUrl || "",
|
|
50
53
|
resume: args.resume || "",
|
|
@@ -66,10 +69,14 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
66
69
|
allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
|
|
67
70
|
allowDestructive: parseBoolean(overrides.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
68
71
|
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, false),
|
|
72
|
+
allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
|
|
69
73
|
allowWrapperTools: parseBoolean(
|
|
70
74
|
overrides.allowWrapperTools ?? args.allowWrapperTools ?? process.env.ALLOW_WRAPPER_TOOLS,
|
|
71
75
|
false
|
|
72
76
|
),
|
|
77
|
+
preferredWrapper: normalizeWrapperName(
|
|
78
|
+
overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
|
|
79
|
+
),
|
|
73
80
|
wrapperTimeoutMs: parseNumber(overrides.wrapperTimeoutMs ?? process.env.WRAPPER_TIMEOUT_MS, 120000),
|
|
74
81
|
sandboxMode,
|
|
75
82
|
packageInstallPolicy: normalizePackageInstallPolicy(
|
|
@@ -84,6 +91,6 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
84
91
|
};
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
export function loadConfig(args) {
|
|
88
|
-
return resolveRuntimeConfig(args);
|
|
94
|
+
export function loadConfig(args, overrides = {}) {
|
|
95
|
+
return resolveRuntimeConfig(args, overrides);
|
|
89
96
|
}
|