@lazyingart/agintiflow 0.6.0 → 0.8.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/README.md +22 -8
- package/docs/agent-runtime-pipe.md +14 -0
- package/package.json +6 -2
- package/public/app.js +40 -5
- package/public/index.html +4 -1
- package/public/styles.css +22 -0
- package/scripts/setup-agent-toolchain-docker.sh +13 -1
- package/scripts/smoke-capabilities.js +4 -0
- package/scripts/smoke-cli-chat.js +76 -0
- package/scripts/smoke-inbox.js +36 -0
- package/scripts/smoke-web-api.js +3 -0
- package/src/agent-runner.js +47 -5
- package/src/capabilities.js +10 -1
- package/src/cli.js +77 -5
- package/src/command-policy.js +33 -9
- package/src/config.js +6 -6
- package/src/docker-sandbox.js +79 -6
- package/src/interactive-cli.js +241 -0
- package/src/model-client.js +3 -1
- package/src/session-store.js +29 -0
- package/src/task-profiles.js +1 -1
- package/src/web-db.js +8 -5
- package/web.js +25 -3
package/src/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ import { listAgentWrappers } from "./tool-wrappers.js";
|
|
|
4
4
|
import { getModelPresets } from "./model-routing.js";
|
|
5
5
|
import { getDockerSandboxStatus, runDockerPreflight } from "./docker-sandbox.js";
|
|
6
6
|
import { buildCapabilityReport, printCapabilityReport } from "./capabilities.js";
|
|
7
|
+
import { startInteractiveCli } from "./interactive-cli.js";
|
|
8
|
+
import { SessionStore } from "./session-store.js";
|
|
7
9
|
import {
|
|
8
10
|
doctorReport,
|
|
9
11
|
initProject,
|
|
@@ -52,9 +54,11 @@ export function parseArgs(argv) {
|
|
|
52
54
|
sandboxStatus: false,
|
|
53
55
|
sandboxPreflight: false,
|
|
54
56
|
web: false,
|
|
57
|
+
interactive: false,
|
|
55
58
|
port: "",
|
|
56
59
|
host: "",
|
|
57
60
|
listProfiles: false,
|
|
61
|
+
latex: false,
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
const parts = [];
|
|
@@ -64,6 +68,10 @@ export function parseArgs(argv) {
|
|
|
64
68
|
result.web = true;
|
|
65
69
|
continue;
|
|
66
70
|
}
|
|
71
|
+
if (arg === "chat" || arg === "interactive" || arg === "--chat" || arg === "--interactive") {
|
|
72
|
+
result.interactive = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
67
75
|
if (arg === "--port") {
|
|
68
76
|
result.port = readOption(argv, i);
|
|
69
77
|
i += 1;
|
|
@@ -124,6 +132,14 @@ export function parseArgs(argv) {
|
|
|
124
132
|
result.sandboxMode = result.sandboxMode || "docker-workspace";
|
|
125
133
|
continue;
|
|
126
134
|
}
|
|
135
|
+
if (arg === "--latex") {
|
|
136
|
+
result.latex = true;
|
|
137
|
+
result.taskProfile = "latex";
|
|
138
|
+
result.sandboxMode = result.sandboxMode || "docker-workspace";
|
|
139
|
+
result.packageInstallPolicy = result.packageInstallPolicy || "allow";
|
|
140
|
+
result.maxSteps = result.maxSteps || 30;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
127
143
|
if (arg === "--max-steps") {
|
|
128
144
|
result.maxSteps = Number(readOption(argv, i));
|
|
129
145
|
i += 1;
|
|
@@ -133,6 +149,10 @@ export function parseArgs(argv) {
|
|
|
133
149
|
result.allowShellTool = true;
|
|
134
150
|
continue;
|
|
135
151
|
}
|
|
152
|
+
if (arg === "--no-shell") {
|
|
153
|
+
result.allowShellTool = false;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
136
156
|
if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
|
|
137
157
|
result.allowDestructive = true;
|
|
138
158
|
continue;
|
|
@@ -197,6 +217,31 @@ export function parseArgs(argv) {
|
|
|
197
217
|
return result;
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
function printUsage() {
|
|
221
|
+
console.log(
|
|
222
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti queue <session-id> "message" OR aginti [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function agentDefaults(args) {
|
|
227
|
+
const defaults = {
|
|
228
|
+
...args,
|
|
229
|
+
allowShellTool: args.allowShellTool ?? true,
|
|
230
|
+
allowFileTools: args.allowFileTools ?? true,
|
|
231
|
+
sandboxMode: args.sandboxMode || "docker-workspace",
|
|
232
|
+
packageInstallPolicy: args.packageInstallPolicy || "allow",
|
|
233
|
+
useDockerSandbox: args.useDockerSandbox ?? true,
|
|
234
|
+
maxSteps: args.maxSteps || (args.latex || args.taskProfile === "latex" ? 30 : 24),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (defaults.sandboxMode === "host") {
|
|
238
|
+
defaults.useDockerSandbox = false;
|
|
239
|
+
defaults.packageInstallPolicy = args.packageInstallPolicy || "prompt";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return defaults;
|
|
243
|
+
}
|
|
244
|
+
|
|
200
245
|
function printRoutes() {
|
|
201
246
|
const presets = getModelPresets();
|
|
202
247
|
for (const preset of Object.values(presets)) {
|
|
@@ -311,7 +356,26 @@ async function handleSessionsCommand(argv) {
|
|
|
311
356
|
process.exit(1);
|
|
312
357
|
}
|
|
313
358
|
|
|
359
|
+
async function handleQueueCommand(argv) {
|
|
360
|
+
const sessionId = argv[0] || "";
|
|
361
|
+
const content = argv.slice(1).join(" ").trim();
|
|
362
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(sessionId) || !content) {
|
|
363
|
+
console.error('Usage: aginti queue <session-id> "message to apply during the next agent step"');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const store = new SessionStore(path.resolve(process.cwd(), ".sessions"), sessionId);
|
|
368
|
+
await store.appendInbox(content, { source: "cli" });
|
|
369
|
+
await store.appendEvent("conversation.queued_input", { prompt: content, source: "cli" }).catch(() => {});
|
|
370
|
+
console.log(`queued message for ${sessionId}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
314
373
|
export async function main(argv = process.argv.slice(2)) {
|
|
374
|
+
if (argv[0] === "--help" || argv[0] === "help" || argv[0] === "-h") {
|
|
375
|
+
printUsage();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
315
379
|
if (argv[0] === "--version" || argv[0] === "version" || argv[0] === "-v") {
|
|
316
380
|
console.log(packageJson.version);
|
|
317
381
|
return;
|
|
@@ -386,6 +450,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
386
450
|
return;
|
|
387
451
|
}
|
|
388
452
|
|
|
453
|
+
if (argv[0] === "queue") {
|
|
454
|
+
await handleQueueCommand(argv.slice(1));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
389
458
|
if (argv[0] === "resume") {
|
|
390
459
|
const sessionId = argv[1] || "";
|
|
391
460
|
const prompt = argv.slice(2).join(" ").trim();
|
|
@@ -393,13 +462,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
393
462
|
console.error('Usage: aginti resume <session-id> "new prompt"');
|
|
394
463
|
process.exit(1);
|
|
395
464
|
}
|
|
396
|
-
const config = loadConfig({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }, { packageDir });
|
|
465
|
+
const config = loadConfig(agentDefaults({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }), { packageDir });
|
|
397
466
|
await runAgent(config);
|
|
398
467
|
return;
|
|
399
468
|
}
|
|
400
469
|
|
|
401
470
|
const args = parseArgs(argv);
|
|
402
471
|
|
|
472
|
+
if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
|
|
473
|
+
await startInteractiveCli(agentDefaults(args), { packageDir, packageVersion: packageJson.version });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
403
477
|
if (args.web) {
|
|
404
478
|
if (args.port) process.env.PORT = String(args.port);
|
|
405
479
|
if (args.host) process.env.HOST = String(args.host);
|
|
@@ -433,12 +507,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
433
507
|
}
|
|
434
508
|
|
|
435
509
|
if (!args.goal && !args.resume) {
|
|
436
|
-
|
|
437
|
-
'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] [--approve-package-installs] [--allow-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
438
|
-
);
|
|
510
|
+
printUsage();
|
|
439
511
|
process.exit(1);
|
|
440
512
|
}
|
|
441
513
|
|
|
442
|
-
const config = loadConfig(args, { packageDir });
|
|
514
|
+
const config = loadConfig(agentDefaults(args), { packageDir });
|
|
443
515
|
await runAgent(config);
|
|
444
516
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -39,6 +39,7 @@ const TEST_PATTERNS = [
|
|
|
39
39
|
];
|
|
40
40
|
|
|
41
41
|
const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
|
|
42
|
+
const PERMISSION_CHANGE_PATTERNS = [/^(?:sudo\s+)?chmod\s+[-+=,rwxugoXst0-7]+\s+[-\w./]+$/];
|
|
42
43
|
|
|
43
44
|
const NETWORK_FETCH_PATTERNS = [
|
|
44
45
|
/^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
|
|
@@ -153,15 +154,6 @@ function classifySimpleCommand(normalized) {
|
|
|
153
154
|
return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
const lowered = ` ${normalized.toLowerCase()} `;
|
|
157
|
-
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
158
|
-
return {
|
|
159
|
-
category: "general-shell",
|
|
160
|
-
needsNetwork: true,
|
|
161
|
-
writesWorkspace: true,
|
|
162
|
-
reason: `Command uses general shell syntax: ${normalized}`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
157
|
if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
|
|
166
158
|
const target = normalized.replace(/^mkdir\s+-p\s+/, "");
|
|
167
159
|
const virtualWorkspacePath = isSafeVirtualWorkspaceDir(target);
|
|
@@ -170,6 +162,16 @@ function classifySimpleCommand(normalized) {
|
|
|
170
162
|
}
|
|
171
163
|
return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
|
|
172
164
|
}
|
|
165
|
+
if (matchAny(PERMISSION_CHANGE_PATTERNS, normalized)) {
|
|
166
|
+
return {
|
|
167
|
+
category: "permission-change",
|
|
168
|
+
needsNetwork: false,
|
|
169
|
+
writesWorkspace: true,
|
|
170
|
+
reason: `Command changes workspace file mode: ${normalized}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lowered = ` ${normalized.toLowerCase()} `;
|
|
173
175
|
if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
|
|
174
176
|
return {
|
|
175
177
|
category: "destructive",
|
|
@@ -178,6 +180,14 @@ function classifySimpleCommand(normalized) {
|
|
|
178
180
|
reason: `Command contains a write-capable or destructive token: ${normalized}`,
|
|
179
181
|
};
|
|
180
182
|
}
|
|
183
|
+
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
184
|
+
return {
|
|
185
|
+
category: "general-shell",
|
|
186
|
+
needsNetwork: true,
|
|
187
|
+
writesWorkspace: true,
|
|
188
|
+
reason: `Command uses general shell syntax: ${normalized}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
181
191
|
|
|
182
192
|
if (matchAny(READ_ONLY_PATTERNS, normalized)) {
|
|
183
193
|
return { category: "read-only", needsNetwork: false, writesWorkspace: false };
|
|
@@ -306,6 +316,20 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
306
316
|
};
|
|
307
317
|
}
|
|
308
318
|
|
|
319
|
+
if (classification.category === "permission-change" && !trustedDockerShell && !trustedHostShell) {
|
|
320
|
+
return {
|
|
321
|
+
allowed: false,
|
|
322
|
+
...classification,
|
|
323
|
+
needsApproval: true,
|
|
324
|
+
reason:
|
|
325
|
+
sandboxMode === "host"
|
|
326
|
+
? "Workspace permission changes on the host require Allow destructive actions. Prefer Docker workspace mode."
|
|
327
|
+
: "Docker permission changes require docker-workspace mode with Package installs = allow.",
|
|
328
|
+
sandboxMode,
|
|
329
|
+
packageInstallPolicy,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
309
333
|
if (classification.category === "destructive" && !config.allowDestructive) {
|
|
310
334
|
return {
|
|
311
335
|
allowed: false,
|
package/src/config.js
CHANGED
|
@@ -42,9 +42,9 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
42
42
|
|
|
43
43
|
const defaults = getProviderDefaults(route.provider);
|
|
44
44
|
const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
|
|
45
|
-
const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX,
|
|
45
|
+
const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, true);
|
|
46
46
|
const requestedSandboxMode =
|
|
47
|
-
overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-
|
|
47
|
+
overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-workspace" : "host");
|
|
48
48
|
const sandboxMode = normalizeSandboxMode(requestedSandboxMode);
|
|
49
49
|
|
|
50
50
|
return {
|
|
@@ -65,14 +65,14 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
65
65
|
apiKey: overrides.apiKey || defaults.apiKey,
|
|
66
66
|
baseURL: overrides.baseURL || defaults.baseURL,
|
|
67
67
|
model: route.model || defaults.model,
|
|
68
|
-
maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS,
|
|
68
|
+
maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, 24),
|
|
69
69
|
headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
|
|
70
70
|
allowedDomains: Array.isArray(overrides.allowedDomains)
|
|
71
71
|
? overrides.allowedDomains
|
|
72
72
|
: parseList(process.env.ALLOWED_DOMAINS),
|
|
73
73
|
allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
|
|
74
74
|
allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
75
|
-
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL,
|
|
75
|
+
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, true),
|
|
76
76
|
allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
|
|
77
77
|
allowWrapperTools: parseBoolean(
|
|
78
78
|
overrides.allowWrapperTools ?? args.allowWrapperTools ?? process.env.ALLOW_WRAPPER_TOOLS,
|
|
@@ -84,9 +84,9 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
84
84
|
wrapperTimeoutMs: parseNumber(overrides.wrapperTimeoutMs ?? process.env.WRAPPER_TIMEOUT_MS, 120000),
|
|
85
85
|
sandboxMode,
|
|
86
86
|
packageInstallPolicy: normalizePackageInstallPolicy(
|
|
87
|
-
overrides.packageInstallPolicy || args.packageInstallPolicy || process.env.PACKAGE_INSTALL_POLICY || "prompt"
|
|
87
|
+
overrides.packageInstallPolicy || args.packageInstallPolicy || process.env.PACKAGE_INSTALL_POLICY || (sandboxMode === "host" ? "prompt" : "allow")
|
|
88
88
|
),
|
|
89
|
-
useDockerSandbox: sandboxMode !== "host"
|
|
89
|
+
useDockerSandbox: sandboxMode !== "host",
|
|
90
90
|
dockerSandboxImage: overrides.dockerSandboxImage || process.env.DOCKER_SANDBOX_IMAGE || "agintiflow-sandbox:latest",
|
|
91
91
|
commandCwd: path.resolve(overrides.commandCwd || args.commandCwd || process.env.COMMAND_CWD || baseDir),
|
|
92
92
|
sessionsDir: path.resolve(baseDir, ".sessions"),
|
package/src/docker-sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { constants as fsConstants } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { execFile as execFileCallback } from "node:child_process";
|
|
5
6
|
import { promisify } from "node:util";
|
|
@@ -8,6 +9,9 @@ import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
|
8
9
|
|
|
9
10
|
const execFile = promisify(execFileCallback);
|
|
10
11
|
const DOCKER_WORKSPACE = "/workspace";
|
|
12
|
+
const DOCKER_HOME = "/aginti-home";
|
|
13
|
+
const DOCKER_CACHE = "/aginti-cache";
|
|
14
|
+
const DOCKER_ENV = "/aginti-env";
|
|
11
15
|
const READY_IMAGES = new Set();
|
|
12
16
|
const SANDBOX_LOG_LIMIT = 80;
|
|
13
17
|
const sandboxLogs = [];
|
|
@@ -101,10 +105,31 @@ function sandboxDockerfilePath(config) {
|
|
|
101
105
|
return path.join(sandboxPackageDir(config), "docker", "sandbox.Dockerfile");
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
function persistentDockerDirs(config) {
|
|
109
|
+
const root = path.resolve(
|
|
110
|
+
config.dockerStateDir || process.env.AGINTIFLOW_DOCKER_STATE_DIR || path.join(os.homedir(), ".agintiflow", "docker")
|
|
111
|
+
);
|
|
112
|
+
return {
|
|
113
|
+
root,
|
|
114
|
+
home: path.join(root, "home"),
|
|
115
|
+
cache: path.join(root, "cache"),
|
|
116
|
+
env: path.join(root, "env"),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function ensurePersistentDockerDirs(config) {
|
|
121
|
+
const dirs = persistentDockerDirs(config);
|
|
122
|
+
await Promise.all([fs.mkdir(dirs.home, { recursive: true }), fs.mkdir(dirs.cache, { recursive: true }), fs.mkdir(dirs.env, { recursive: true })]);
|
|
123
|
+
return dirs;
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
export async function getDockerSandboxStatus(config) {
|
|
105
127
|
const image = config.dockerSandboxImage;
|
|
106
128
|
const dockerfilePath = sandboxDockerfilePath(config);
|
|
107
129
|
const workspace = config.commandCwd;
|
|
130
|
+
if (config.useDockerSandbox) {
|
|
131
|
+
await ensurePersistentDockerDirs(config).catch(() => null);
|
|
132
|
+
}
|
|
108
133
|
const [dockerReady, dockerfileExists, workspaceExists, workspaceReadable, workspaceWritable] = await Promise.all([
|
|
109
134
|
dockerAvailable(),
|
|
110
135
|
pathAccess(dockerfilePath, fsConstants.R_OK),
|
|
@@ -114,6 +139,12 @@ export async function getDockerSandboxStatus(config) {
|
|
|
114
139
|
]);
|
|
115
140
|
|
|
116
141
|
const imageReady = dockerReady ? await dockerImageExists(image) : false;
|
|
142
|
+
const persistentDirs = persistentDockerDirs(config);
|
|
143
|
+
const [persistentHomeExists, persistentCacheExists, persistentEnvExists] = await Promise.all([
|
|
144
|
+
pathAccess(persistentDirs.home, fsConstants.F_OK),
|
|
145
|
+
pathAccess(persistentDirs.cache, fsConstants.F_OK),
|
|
146
|
+
pathAccess(persistentDirs.env, fsConstants.F_OK),
|
|
147
|
+
]);
|
|
117
148
|
return {
|
|
118
149
|
sandboxMode: normalizeSandboxMode(config.sandboxMode),
|
|
119
150
|
useDockerSandbox: Boolean(config.useDockerSandbox),
|
|
@@ -127,6 +158,18 @@ export async function getDockerSandboxStatus(config) {
|
|
|
127
158
|
workspaceReadable,
|
|
128
159
|
workspaceWritable,
|
|
129
160
|
dockerfileExists,
|
|
161
|
+
persistentDocker: {
|
|
162
|
+
home: persistentDirs.home,
|
|
163
|
+
cache: persistentDirs.cache,
|
|
164
|
+
env: persistentDirs.env,
|
|
165
|
+
homeExists: persistentHomeExists,
|
|
166
|
+
cacheExists: persistentCacheExists,
|
|
167
|
+
envExists: persistentEnvExists,
|
|
168
|
+
containerHome: DOCKER_HOME,
|
|
169
|
+
containerCache: DOCKER_CACHE,
|
|
170
|
+
containerEnv: DOCKER_ENV,
|
|
171
|
+
pythonVenv: `${DOCKER_ENV}/python`,
|
|
172
|
+
},
|
|
130
173
|
logs: getSandboxLogs(),
|
|
131
174
|
};
|
|
132
175
|
}
|
|
@@ -165,7 +208,30 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
|
|
|
165
208
|
await buildDockerSandboxImage(config, observers);
|
|
166
209
|
}
|
|
167
210
|
|
|
168
|
-
function
|
|
211
|
+
function dockerCommand(command, policy) {
|
|
212
|
+
const envLines = [
|
|
213
|
+
`export HOME=${DOCKER_HOME}`,
|
|
214
|
+
`export XDG_CACHE_HOME=${DOCKER_CACHE}`,
|
|
215
|
+
`export PIP_CACHE_DIR=${DOCKER_CACHE}/pip`,
|
|
216
|
+
`export UV_CACHE_DIR=${DOCKER_CACHE}/uv`,
|
|
217
|
+
`export NPM_CONFIG_CACHE=${DOCKER_CACHE}/npm`,
|
|
218
|
+
`export CONDA_PKGS_DIRS=${DOCKER_CACHE}/conda-pkgs`,
|
|
219
|
+
`export PYTHONUSERBASE=${DOCKER_ENV}/python-user`,
|
|
220
|
+
`export PATH=${DOCKER_ENV}/python/bin:${DOCKER_ENV}/python-user/bin:${DOCKER_ENV}/miniforge/bin:${DOCKER_ENV}/bin:$PATH`,
|
|
221
|
+
`cd ${DOCKER_WORKSPACE}`,
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
if (!policy.requiresDockerRoot) {
|
|
225
|
+
envLines.push(
|
|
226
|
+
`if command -v python3 >/dev/null 2>&1 && [ ! -x ${DOCKER_ENV}/python/bin/python ]; then python3 -m venv --system-site-packages ${DOCKER_ENV}/python >/dev/null 2>&1 || true; fi`,
|
|
227
|
+
`if [ -f ${DOCKER_ENV}/python/bin/activate ]; then . ${DOCKER_ENV}/python/bin/activate; fi`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return [...envLines, String(command)].join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config), persistentDirs = persistentDockerDirs(config)) {
|
|
169
235
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
170
236
|
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
171
237
|
const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
|
|
@@ -196,24 +262,29 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
196
262
|
...readOnlyArgs,
|
|
197
263
|
...userArgs,
|
|
198
264
|
"-e",
|
|
199
|
-
"HOME=/tmp",
|
|
200
|
-
"-e",
|
|
201
265
|
"NPM_CONFIG_USERCONFIG=/tmp/.npmrc",
|
|
202
266
|
"-e",
|
|
203
267
|
"PIP_DISABLE_PIP_VERSION_CHECK=1",
|
|
204
268
|
"-v",
|
|
205
269
|
`${config.commandCwd}:${DOCKER_WORKSPACE}:${mountMode}`,
|
|
270
|
+
"-v",
|
|
271
|
+
`${persistentDirs.home}:${DOCKER_HOME}:rw`,
|
|
272
|
+
"-v",
|
|
273
|
+
`${persistentDirs.cache}:${DOCKER_CACHE}:rw`,
|
|
274
|
+
"-v",
|
|
275
|
+
`${persistentDirs.env}:${DOCKER_ENV}:rw`,
|
|
206
276
|
"-w",
|
|
207
277
|
DOCKER_WORKSPACE,
|
|
208
278
|
config.dockerSandboxImage,
|
|
209
279
|
"bash",
|
|
210
280
|
"-lc",
|
|
211
|
-
|
|
281
|
+
dockerCommand(command, policy),
|
|
212
282
|
];
|
|
213
283
|
}
|
|
214
284
|
|
|
215
285
|
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
216
|
-
const
|
|
286
|
+
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
287
|
+
const result = await execDocker(dockerRunArgs(command, config, policy, persistentDirs), {
|
|
217
288
|
timeout: policy.needsNetwork ? 120000 : policy.category === "toolchain" ? 90000 : 15000,
|
|
218
289
|
maxBuffer: 300 * 1024,
|
|
219
290
|
});
|
|
@@ -227,6 +298,7 @@ export async function runDockerSandboxCommand(command, config, policy = evaluate
|
|
|
227
298
|
category: policy.category,
|
|
228
299
|
sandboxMode: policy.sandboxMode,
|
|
229
300
|
network: policy.needsNetwork ? "bridge" : "none",
|
|
301
|
+
persistentDocker: persistentDirs,
|
|
230
302
|
stdout: payload.stdout.slice(0, 1200),
|
|
231
303
|
stderr: payload.stderr.slice(0, 1200),
|
|
232
304
|
});
|
|
@@ -274,6 +346,7 @@ export async function runDockerPreflight(config, options = {}) {
|
|
|
274
346
|
|
|
275
347
|
let status = await getDockerSandboxStatus(config);
|
|
276
348
|
const runChecks = async () => {
|
|
349
|
+
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
277
350
|
const results = [];
|
|
278
351
|
for (const command of [
|
|
279
352
|
"node -v",
|
|
@@ -287,7 +360,7 @@ export async function runDockerPreflight(config, options = {}) {
|
|
|
287
360
|
"rg --version",
|
|
288
361
|
]) {
|
|
289
362
|
try {
|
|
290
|
-
const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }), {
|
|
363
|
+
const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }, persistentDirs), {
|
|
291
364
|
timeout: 15000,
|
|
292
365
|
maxBuffer: 100 * 1024,
|
|
293
366
|
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { runAgent } from "./agent-runner.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { initProject, listProjectSessions } from "./project.js";
|
|
6
|
+
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
|
|
7
|
+
import { normalizeTaskProfile } from "./task-profiles.js";
|
|
8
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
console.log(
|
|
11
|
+
[
|
|
12
|
+
"Commands:",
|
|
13
|
+
" /help Show this help.",
|
|
14
|
+
" /status Show active route, workspace, sandbox, and session.",
|
|
15
|
+
" /new Start a fresh session on the next message.",
|
|
16
|
+
" /resume <session-id> Continue a saved session.",
|
|
17
|
+
" /sessions List recent sessions in this project.",
|
|
18
|
+
" /profile <name> Set task profile, e.g. code, website, latex, maintenance.",
|
|
19
|
+
" /routing <mode> Set routing: smart, fast, complex, manual.",
|
|
20
|
+
" /provider <name> Set provider: deepseek, openai, mock.",
|
|
21
|
+
" /model <name> Set an explicit model, or /model auto.",
|
|
22
|
+
" /docker on Use docker-workspace with approved package installs.",
|
|
23
|
+
" /docker off Use host shell policy.",
|
|
24
|
+
" /latex on Use the LaTeX/PDF profile in Docker with a larger step budget.",
|
|
25
|
+
" /installs block|prompt|allow",
|
|
26
|
+
" /cwd <path> Change command workspace.",
|
|
27
|
+
" /exit Quit.",
|
|
28
|
+
"",
|
|
29
|
+
"Type a normal request to run the agent. Example: write a Python CLI app with tests",
|
|
30
|
+
].join("\n")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printStatus(state) {
|
|
35
|
+
console.log(`project=${process.cwd()}`);
|
|
36
|
+
console.log(`cwd=${state.commandCwd || process.cwd()}`);
|
|
37
|
+
console.log(`session=${state.sessionId || "new"}`);
|
|
38
|
+
console.log(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
|
|
39
|
+
console.log(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
|
|
40
|
+
console.log(
|
|
41
|
+
`shell=${state.allowShellTool} files=${state.allowFileTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
|
|
42
|
+
);
|
|
43
|
+
if (state.sandboxMode !== "host") {
|
|
44
|
+
console.log(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createState(args = {}) {
|
|
49
|
+
return {
|
|
50
|
+
provider: args.provider || "",
|
|
51
|
+
model: args.model || "",
|
|
52
|
+
routingMode: args.routingMode || "smart",
|
|
53
|
+
commandCwd: args.commandCwd || process.cwd(),
|
|
54
|
+
sandboxMode: normalizeSandboxMode(args.sandboxMode || "docker-workspace"),
|
|
55
|
+
packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "allow"),
|
|
56
|
+
allowShellTool: args.allowShellTool ?? true,
|
|
57
|
+
allowFileTools: args.allowFileTools ?? true,
|
|
58
|
+
allowWrapperTools: args.allowWrapperTools ?? false,
|
|
59
|
+
allowDestructive: args.allowDestructive ?? false,
|
|
60
|
+
preferredWrapper: args.preferredWrapper || "codex",
|
|
61
|
+
taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
|
|
62
|
+
headless: args.headless ?? false,
|
|
63
|
+
maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : args.latex ? 30 : 24,
|
|
64
|
+
sessionId: args.resume || "",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleCommand(line, state, packageDir) {
|
|
69
|
+
const [command, ...rest] = line.slice(1).trim().split(/\s+/);
|
|
70
|
+
const value = rest.join(" ").trim();
|
|
71
|
+
|
|
72
|
+
if (!command || command === "help" || command === "?") {
|
|
73
|
+
printHelp();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (command === "exit" || command === "quit" || command === "q") return false;
|
|
77
|
+
if (command === "status") {
|
|
78
|
+
printStatus(state);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (command === "new") {
|
|
82
|
+
state.sessionId = "";
|
|
83
|
+
console.log("Next message will start a new session.");
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (command === "resume") {
|
|
87
|
+
if (!value) console.log("Usage: /resume <session-id>");
|
|
88
|
+
else {
|
|
89
|
+
state.sessionId = value;
|
|
90
|
+
console.log(`Resuming ${state.sessionId}`);
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (command === "sessions") {
|
|
95
|
+
const sessions = await listProjectSessions(process.cwd(), 20);
|
|
96
|
+
if (sessions.length === 0) console.log("No project-local sessions found.");
|
|
97
|
+
else {
|
|
98
|
+
for (const session of sessions) {
|
|
99
|
+
const goal = session.goal ? ` ${session.goal.slice(0, 80)}` : "";
|
|
100
|
+
console.log(`${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (command === "profile") {
|
|
106
|
+
state.taskProfile = normalizeTaskProfile(value || "auto");
|
|
107
|
+
console.log(`profile=${state.taskProfile}`);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (command === "routing") {
|
|
111
|
+
state.routingMode = value || "smart";
|
|
112
|
+
console.log(`routing=${state.routingMode}`);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (command === "provider") {
|
|
116
|
+
state.provider = value === "auto" ? "" : value;
|
|
117
|
+
console.log(`provider=${state.provider || "auto"}`);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (command === "model") {
|
|
121
|
+
state.model = value === "auto" ? "" : value;
|
|
122
|
+
console.log(`model=${state.model || "auto"}`);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (command === "installs") {
|
|
126
|
+
state.packageInstallPolicy = normalizePackageInstallPolicy(value || "prompt");
|
|
127
|
+
console.log(`installs=${state.packageInstallPolicy}`);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (command === "docker") {
|
|
131
|
+
if (value === "on") {
|
|
132
|
+
state.sandboxMode = "docker-workspace";
|
|
133
|
+
state.packageInstallPolicy = "allow";
|
|
134
|
+
console.log(`docker=on /workspace -> ${state.commandCwd || process.cwd()} installs=allow`);
|
|
135
|
+
} else if (value === "off") {
|
|
136
|
+
state.sandboxMode = "host";
|
|
137
|
+
state.packageInstallPolicy = "prompt";
|
|
138
|
+
console.log("docker=off sandbox=host installs=prompt");
|
|
139
|
+
} else {
|
|
140
|
+
console.log("Usage: /docker on OR /docker off");
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (command === "latex") {
|
|
145
|
+
if (value === "on" || value === "") {
|
|
146
|
+
state.taskProfile = "latex";
|
|
147
|
+
state.sandboxMode = "docker-workspace";
|
|
148
|
+
state.packageInstallPolicy = "allow";
|
|
149
|
+
state.maxSteps = Math.max(state.maxSteps, 30);
|
|
150
|
+
console.log("latex=on profile=latex sandbox=docker-workspace installs=allow maxSteps=30");
|
|
151
|
+
} else if (value === "off") {
|
|
152
|
+
state.taskProfile = "auto";
|
|
153
|
+
console.log("latex=off profile=auto");
|
|
154
|
+
} else {
|
|
155
|
+
console.log("Usage: /latex on OR /latex off");
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (command === "cwd") {
|
|
160
|
+
state.commandCwd = value || process.cwd();
|
|
161
|
+
console.log(`cwd=${state.commandCwd}`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (command === "init") {
|
|
165
|
+
const result = await initProject(process.cwd());
|
|
166
|
+
console.log(`initialized project=${result.projectRoot}`);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (command === "web") {
|
|
170
|
+
const port = value || "3220";
|
|
171
|
+
console.log(`Run in another terminal: aginti web --port ${port}`);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`Unknown command: /${command}. Use /help.`);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function runPrompt(prompt, state, packageDir) {
|
|
180
|
+
const config = loadConfig(
|
|
181
|
+
{
|
|
182
|
+
provider: state.provider,
|
|
183
|
+
model: state.model,
|
|
184
|
+
routingMode: state.routingMode,
|
|
185
|
+
commandCwd: state.commandCwd,
|
|
186
|
+
sandboxMode: state.sandboxMode,
|
|
187
|
+
packageInstallPolicy: state.packageInstallPolicy,
|
|
188
|
+
allowShellTool: state.allowShellTool,
|
|
189
|
+
allowFileTools: state.allowFileTools,
|
|
190
|
+
allowWrapperTools: state.allowWrapperTools,
|
|
191
|
+
allowDestructive: state.allowDestructive,
|
|
192
|
+
preferredWrapper: state.preferredWrapper,
|
|
193
|
+
taskProfile: state.taskProfile,
|
|
194
|
+
maxSteps: state.maxSteps,
|
|
195
|
+
headless: state.headless,
|
|
196
|
+
resume: state.sessionId,
|
|
197
|
+
goal: prompt,
|
|
198
|
+
},
|
|
199
|
+
{ packageDir, baseDir: process.cwd() }
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await runAgent(config);
|
|
203
|
+
state.sessionId = result.sessionId || state.sessionId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
207
|
+
const state = createState(args);
|
|
208
|
+
const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
|
|
209
|
+
|
|
210
|
+
console.log(`AgInTiFlow ${packageVersion || ""}`.trim());
|
|
211
|
+
console.log(`Project: ${process.cwd()}`);
|
|
212
|
+
console.log("Interactive agent chat. Type /help for commands, /exit to quit.");
|
|
213
|
+
printStatus(state);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
let answer = "";
|
|
218
|
+
try {
|
|
219
|
+
answer = await rl.question("\naginti> ");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error?.code === "ERR_USE_AFTER_CLOSE") break;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
const line = answer.trim();
|
|
225
|
+
if (!line) continue;
|
|
226
|
+
if (line.startsWith("/")) {
|
|
227
|
+
const keepGoing = await handleCommand(line, state, packageDir);
|
|
228
|
+
if (!keepGoing) break;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await runPrompt(line, state, packageDir);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(`error: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
rl.close();
|
|
240
|
+
}
|
|
241
|
+
}
|