@lazyingart/agintiflow 0.5.0 → 0.7.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 +19 -8
- package/docs/real-deepseek-capabilities.md +2 -2
- package/package.json +4 -2
- package/scripts/real-deepseek-capabilities.js +3 -3
- package/scripts/smoke-capabilities.js +9 -1
- package/scripts/smoke-cli-chat.js +76 -0
- package/scripts/smoke-web-api.js +3 -0
- package/src/agent-runner.js +14 -4
- package/src/capabilities.js +38 -0
- package/src/cli.js +44 -5
- package/src/command-policy.js +100 -12
- package/src/config.js +1 -1
- package/src/docker-sandbox.js +1 -1
- package/src/interactive-cli.js +225 -0
- package/src/model-client.js +1 -1
- package/src/task-profiles.js +2 -2
package/README.md
CHANGED
|
@@ -46,6 +46,16 @@ aginti --list-profiles
|
|
|
46
46
|
aginti --sandbox-status --sandbox-mode docker-readonly
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Start an interactive Codex-style CLI chat from any project folder:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
aginti
|
|
53
|
+
# or explicitly:
|
|
54
|
+
aginti chat
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Inside chat, type normal requests such as `write a small Python CLI app with tests`. Use `/help` for commands, `/docker on` to switch to Docker workspace mode with approved package installs, `/sessions` to list project runs, and `/resume <session-id>` to continue work.
|
|
58
|
+
|
|
49
59
|
Launch the local web UI from an installed package:
|
|
50
60
|
|
|
51
61
|
```bash
|
|
@@ -71,7 +81,8 @@ aginti doctor --capabilities
|
|
|
71
81
|
aginti sessions list
|
|
72
82
|
aginti sessions show <session-id>
|
|
73
83
|
aginti resume <session-id> "continue with a short follow-up"
|
|
74
|
-
aginti --profile code
|
|
84
|
+
aginti --profile code "write a small Python CLI app with tests"
|
|
85
|
+
aginti --sandbox-mode docker-workspace --approve-package-installs "set up this project and run the tests"
|
|
75
86
|
```
|
|
76
87
|
|
|
77
88
|
Run from a source checkout:
|
|
@@ -181,12 +192,12 @@ AgInTiFlow is intentionally conservative:
|
|
|
181
192
|
- Shell commands are disabled unless the shell tool is enabled.
|
|
182
193
|
- Workspace file tools stay inside `commandCwd` and block `.env`, secret-like paths, `.git`, node_modules writes, absolute escapes, binary files, and huge files.
|
|
183
194
|
- File writes record before/after SHA-256 hashes and compact redacted diffs.
|
|
184
|
-
- Guarded shell mode
|
|
195
|
+
- Guarded shell mode is policy-based: normal inspection/tests are available, Docker workspace mode can run broader setup/network commands when package installs are approved, and host privileged/destructive work requires explicit trust.
|
|
185
196
|
- Docker read-only mode mounts the workspace read-only and disables container network access.
|
|
186
197
|
- Docker workspace-write mode is the web UI default so plot, PDF, and test outputs can be written inside the mounted workspace.
|
|
187
|
-
- Docker workspace-write mode is
|
|
198
|
+
- Docker workspace-write mode is the preferred place for approved package or environment setup, including `apt-get`, `npm`, `pip`, `curl`, and `wget` workflows inside the container.
|
|
188
199
|
- Package installs default to `prompt`, so npm/pip/conda/venv setup is blocked until explicitly approved.
|
|
189
|
-
- NPM publishing, npm token commands,
|
|
200
|
+
- NPM publishing, npm token commands, API-key reads, and credential files stay blocked. Broader shell commands require either Docker workspace mode with package policy `allow`, or explicit host trust.
|
|
190
201
|
- NPM tokens and API keys are redacted from tool logs and API responses.
|
|
191
202
|
- Every tool request and result is written to structured logs.
|
|
192
203
|
|
|
@@ -311,9 +322,9 @@ The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sa
|
|
|
311
322
|
|
|
312
323
|
| Mode | Workspace mount | Network | Intended use |
|
|
313
324
|
| --- | --- | --- | --- |
|
|
314
|
-
| `host` | local process | host network |
|
|
325
|
+
| `host` | local process | host network | direct project work; privileged/destructive commands require explicit trust |
|
|
315
326
|
| `docker-readonly` | read-only `/workspace` | none | safe coding inspection and tests that do not write files |
|
|
316
|
-
| `docker-workspace` | writable `/workspace` | none by default, enabled
|
|
327
|
+
| `docker-workspace` | writable `/workspace` | none by default, enabled for approved package/setup/network commands | web UI default for environment setup, plotting, LaTeX/PDF, dependency installs, and broad toolchain work inside the mounted project |
|
|
317
328
|
|
|
318
329
|
Package policy values:
|
|
319
330
|
|
|
@@ -321,9 +332,9 @@ Package policy values:
|
|
|
321
332
|
| --- | --- |
|
|
322
333
|
| `block` | Always block npm/pip/conda/venv setup. |
|
|
323
334
|
| `prompt` | Return a clear approval-required error; the UI can switch to approved. |
|
|
324
|
-
| `allow` | Permit
|
|
335
|
+
| `allow` | Permit package/setup commands. Docker workspace mode also allows broader shell/network commands while keeping secrets and npm publishing blocked. |
|
|
325
336
|
|
|
326
|
-
Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode
|
|
337
|
+
Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode the project folder is mounted as `/workspace`; any file written to `/workspace/report.pdf` appears on the host as `<your-project>/report.pdf`. CLI runs print both the host workspace and the Docker mapping before execution. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
|
|
327
338
|
|
|
328
339
|
Safe preflight endpoints:
|
|
329
340
|
|
|
@@ -43,8 +43,8 @@ AGINTIFLOW_REAL_DEEPSEEK=1 AGINTIFLOW_REAL_CASES=flash,pro npm run real:deepseek
|
|
|
43
43
|
- `flash`: DeepSeek v4 flash creates a dependency-free Node/HTML app with `node:test` tests and runs `npm --prefix <app> test` when safe.
|
|
44
44
|
- `pro`: DeepSeek v4 pro resumes/improves the app and expands tests.
|
|
45
45
|
- `latex`: creates `.tex` source and compiles only if a TeX toolchain is available; otherwise writes an honest setup artifact.
|
|
46
|
-
- `website`: creates a
|
|
47
|
-
- `maintenance`:
|
|
46
|
+
- `website`: creates a website test sample and runs checks with the configured shell/package policy.
|
|
47
|
+
- `maintenance`: verifies the agent can prepare project-local maintenance work and use Docker/package policy for broader setup commands without exposing secrets.
|
|
48
48
|
- `aaps`: creates a project-local `.aaps` sample and notes for `@lazyingart/aaps` workflows without publishing.
|
|
49
49
|
- `web`: when `AGINTIFLOW_REAL_WEB_BASE_URL` is set, starts a web run and verifies CLI/web session sharing.
|
|
50
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"scripts/install-docker-ubuntu.sh",
|
|
42
42
|
"scripts/setup-agent-toolchain-docker.sh",
|
|
43
43
|
"scripts/real-deepseek-capabilities.js",
|
|
44
|
+
"scripts/smoke-cli-chat.js",
|
|
44
45
|
"scripts/smoke-coding-tools.js",
|
|
45
46
|
"scripts/smoke-capabilities.js",
|
|
46
47
|
"scripts/smoke-toolchain-docker.js",
|
|
@@ -59,10 +60,11 @@
|
|
|
59
60
|
"check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js",
|
|
60
61
|
"setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
|
|
61
62
|
"smoke:coding-tools": "node scripts/smoke-coding-tools.js",
|
|
63
|
+
"smoke:cli-chat": "node scripts/smoke-cli-chat.js",
|
|
62
64
|
"smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
|
|
63
65
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
64
66
|
"real:deepseek": "node scripts/real-deepseek-capabilities.js",
|
|
65
|
-
"test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities",
|
|
67
|
+
"test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities && npm run smoke:cli-chat",
|
|
66
68
|
"pack:dry-run": "npm pack --dry-run",
|
|
67
69
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
68
70
|
},
|
|
@@ -308,7 +308,7 @@ if (cases.has("website")) {
|
|
|
308
308
|
const websiteDir = `round9/website-test-${stamp}`;
|
|
309
309
|
const run = await runCliCase(
|
|
310
310
|
"website-test",
|
|
311
|
-
`Create a small website under ${websiteDir} with a simple local test or check file. Run
|
|
311
|
+
`Create a small website under ${websiteDir} with a simple local test or check file. Run useful checks under the configured policy.`,
|
|
312
312
|
{ routingMode: "fast", taskProfile: "website", maxSteps: 24 }
|
|
313
313
|
);
|
|
314
314
|
report.runs.push(run);
|
|
@@ -333,12 +333,12 @@ if (cases.has("maintenance")) {
|
|
|
333
333
|
const maintenanceDir = `maintenance/round9-${stamp}`;
|
|
334
334
|
const run = await runCliCase(
|
|
335
335
|
"maintenance-plan",
|
|
336
|
-
`Create project-local
|
|
336
|
+
`Create project-local maintenance plans and scripts under ${maintenanceDir} for Miniforge/conda, R, Python tooling, CmdStan/CmdStanR, and PyStan. Use Docker/package policy for setup checks when useful.`,
|
|
337
337
|
{
|
|
338
338
|
routingMode: "complex",
|
|
339
339
|
taskProfile: "maintenance",
|
|
340
340
|
sandboxMode: "docker-workspace",
|
|
341
|
-
packageInstallPolicy: "
|
|
341
|
+
packageInstallPolicy: "allow",
|
|
342
342
|
maxSteps: 24,
|
|
343
343
|
}
|
|
344
344
|
);
|
|
@@ -54,6 +54,14 @@ try {
|
|
|
54
54
|
capabilities.maintenancePolicy.some((check) => check.command.startsWith("sudo") && !check.allowed),
|
|
55
55
|
"sudo maintenance command was not blocked"
|
|
56
56
|
);
|
|
57
|
+
assert(
|
|
58
|
+
capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("apt-get install") && check.allowed),
|
|
59
|
+
"trusted Docker policy did not allow apt-get install"
|
|
60
|
+
);
|
|
61
|
+
assert(
|
|
62
|
+
capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("wget") && check.allowed),
|
|
63
|
+
"trusted Docker policy did not allow wget"
|
|
64
|
+
);
|
|
57
65
|
|
|
58
66
|
const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
|
|
59
67
|
assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
|
|
@@ -63,7 +71,7 @@ try {
|
|
|
63
71
|
{
|
|
64
72
|
ok: true,
|
|
65
73
|
projectRoot: tempRoot,
|
|
66
|
-
checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy"],
|
|
74
|
+
checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
|
|
67
75
|
},
|
|
68
76
|
null,
|
|
69
77
|
2
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-cli-chat-"));
|
|
10
|
+
const binPath = path.join(repoRoot, "bin/aginti-cli.js");
|
|
11
|
+
|
|
12
|
+
function runChat(inputText) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const child = spawn(
|
|
15
|
+
process.execPath,
|
|
16
|
+
[binPath, "chat", "--provider", "mock", "--routing", "manual", "--profile", "code"],
|
|
17
|
+
{
|
|
18
|
+
cwd: tempRoot,
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
AGINTIFLOW_RUNTIME_DIR: "",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
child.kill("SIGTERM");
|
|
31
|
+
reject(new Error("interactive chat smoke timed out"));
|
|
32
|
+
}, 25000);
|
|
33
|
+
|
|
34
|
+
child.stdout.on("data", (chunk) => {
|
|
35
|
+
stdout += String(chunk);
|
|
36
|
+
});
|
|
37
|
+
child.stderr.on("data", (chunk) => {
|
|
38
|
+
stderr += String(chunk);
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (error) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(error);
|
|
43
|
+
});
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (code === 0) resolve({ stdout, stderr });
|
|
47
|
+
else reject(new Error(`interactive chat exited ${code}\n${stdout}\n${stderr}`));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
child.stdin.end(inputText);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await runChat("Create notes/interactive.md with a short CLI chat smoke message\n/exit\n");
|
|
56
|
+
const written = await fs.readFile(path.join(tempRoot, "notes/interactive.md"), "utf8");
|
|
57
|
+
if (!written.includes("Created by AgInTiFlow mock mode.")) {
|
|
58
|
+
throw new Error("interactive chat did not create the expected file");
|
|
59
|
+
}
|
|
60
|
+
if (!result.stdout.includes("Interactive agent chat")) {
|
|
61
|
+
throw new Error("interactive chat did not print its banner");
|
|
62
|
+
}
|
|
63
|
+
console.log(
|
|
64
|
+
JSON.stringify(
|
|
65
|
+
{
|
|
66
|
+
ok: true,
|
|
67
|
+
projectRoot: tempRoot,
|
|
68
|
+
checks: ["interactive-chat", "mock-file-write"],
|
|
69
|
+
},
|
|
70
|
+
null,
|
|
71
|
+
2
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
76
|
+
}
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -90,6 +90,9 @@ try {
|
|
|
90
90
|
if (!capabilities.checks.some((check) => check.name === "npm-prefix-test-policy")) {
|
|
91
91
|
throw new Error("capability endpoint did not include command policy checks");
|
|
92
92
|
}
|
|
93
|
+
if (!capabilities.trustedDockerPolicy?.some((check) => check.command.startsWith("apt-get install") && check.allowed)) {
|
|
94
|
+
throw new Error("capability endpoint did not report trusted Docker package policy");
|
|
95
|
+
}
|
|
93
96
|
const savedKey = await fetchJson("/api/keys/deepseek", {
|
|
94
97
|
method: "POST",
|
|
95
98
|
headers: { "Content-Type": "application/json" },
|
package/src/agent-runner.js
CHANGED
|
@@ -144,8 +144,8 @@ function createInitialState(config, sessionId) {
|
|
|
144
144
|
"Avoid destructive actions, purchases, account changes, and sensitive workflows.",
|
|
145
145
|
config.allowShellTool
|
|
146
146
|
? config.useDockerSandbox
|
|
147
|
-
? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}.
|
|
148
|
-
: "A
|
|
147
|
+
? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Docker workspace mode with approved package installs supports broader setup and network commands.`
|
|
148
|
+
: "A host shell command tool is available under the configured trust policy."
|
|
149
149
|
: "No shell command tool is available.",
|
|
150
150
|
config.allowFileTools
|
|
151
151
|
? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, and apply_patch. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked.`
|
|
@@ -158,7 +158,7 @@ function createInitialState(config, sessionId) {
|
|
|
158
158
|
"For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
|
|
159
159
|
"Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
|
|
160
160
|
"Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
|
|
161
|
-
"For environment or system-maintenance work,
|
|
161
|
+
"For environment or system-maintenance work, use the configured sandbox and package policy; Docker workspace mode is the preferred place for installs and toolchain setup.",
|
|
162
162
|
"When the requested outcome is complete and a useful check has passed or been honestly skipped, stop and call finish.",
|
|
163
163
|
"When done, call finish with a concise result.",
|
|
164
164
|
].join(" "),
|
|
@@ -182,7 +182,7 @@ function createInitialState(config, sessionId) {
|
|
|
182
182
|
"Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
|
|
183
183
|
"Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
|
|
184
184
|
"Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
|
|
185
|
-
"
|
|
185
|
+
"Use the configured sandbox and package policy for environment or system-maintenance work.",
|
|
186
186
|
]
|
|
187
187
|
.filter(Boolean)
|
|
188
188
|
.join("\n"),
|
|
@@ -791,6 +791,16 @@ export async function runAgent(config) {
|
|
|
791
791
|
console.log(`Provider: ${config.provider}`);
|
|
792
792
|
console.log(`Model: ${config.model}`);
|
|
793
793
|
console.log(`Routing: ${config.routingMode} (${config.routeReason})`);
|
|
794
|
+
console.log(`Workspace: ${config.commandCwd}`);
|
|
795
|
+
console.log(`Sessions: ${config.sessionsDir}`);
|
|
796
|
+
if (config.useDockerSandbox) {
|
|
797
|
+
console.log(
|
|
798
|
+
`Docker: image=${config.dockerSandboxImage} mode=${config.sandboxMode} packagePolicy=${config.packageInstallPolicy}`
|
|
799
|
+
);
|
|
800
|
+
console.log(`Docker workspace: /workspace -> ${config.commandCwd}`);
|
|
801
|
+
} else if (config.allowShellTool) {
|
|
802
|
+
console.log(`Shell: host policy=${config.packageInstallPolicy}`);
|
|
803
|
+
}
|
|
794
804
|
if (state.plan) {
|
|
795
805
|
console.log("\nPlan:");
|
|
796
806
|
console.log(state.plan);
|
package/src/capabilities.js
CHANGED
|
@@ -64,6 +64,39 @@ function maintenancePolicyChecks(config) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function trustedDockerPolicyChecks(config) {
|
|
68
|
+
const dockerConfig = {
|
|
69
|
+
...config,
|
|
70
|
+
allowShellTool: true,
|
|
71
|
+
allowDestructive: false,
|
|
72
|
+
sandboxMode: "docker-workspace",
|
|
73
|
+
useDockerSandbox: true,
|
|
74
|
+
packageInstallPolicy: "allow",
|
|
75
|
+
};
|
|
76
|
+
const sampleCommands = [
|
|
77
|
+
"apt-get update",
|
|
78
|
+
"apt-get install -y curl wget",
|
|
79
|
+
"wget https://example.com/file.txt",
|
|
80
|
+
"curl -fsSL https://example.com/file.txt -o downloads/file.txt",
|
|
81
|
+
"npm install lodash",
|
|
82
|
+
"python3 -m pip install requests",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return sampleCommands.map((command) => {
|
|
86
|
+
const policy = evaluateCommandPolicy(command, dockerConfig);
|
|
87
|
+
return {
|
|
88
|
+
command,
|
|
89
|
+
allowed: Boolean(policy.allowed),
|
|
90
|
+
category: policy.category || classifyCommand(command).category,
|
|
91
|
+
reason: policy.reason || "",
|
|
92
|
+
needsNetwork: Boolean(policy.needsNetwork),
|
|
93
|
+
requiresDockerRoot: Boolean(policy.requiresDockerRoot),
|
|
94
|
+
sandboxMode: policy.sandboxMode,
|
|
95
|
+
packageInstallPolicy: policy.packageInstallPolicy,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
export async function buildCapabilityReport(projectRoot, packageVersion, config) {
|
|
68
101
|
const paths = projectPaths(projectRoot);
|
|
69
102
|
const keyStatus = providerKeyStatus(projectRoot);
|
|
@@ -155,6 +188,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
155
188
|
},
|
|
156
189
|
checks,
|
|
157
190
|
maintenancePolicy: maintenancePolicyChecks(config),
|
|
191
|
+
trustedDockerPolicy: trustedDockerPolicyChecks(config),
|
|
158
192
|
sessions,
|
|
159
193
|
actionableSetup: checks
|
|
160
194
|
.filter((check) => !check.ok && check.setup)
|
|
@@ -184,6 +218,10 @@ export function printCapabilityReport(report) {
|
|
|
184
218
|
const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
|
|
185
219
|
console.log(`${check.ok ? "OK" : "MISS"} ${check.name}${suffix}`);
|
|
186
220
|
}
|
|
221
|
+
if (report.trustedDockerPolicy?.length) {
|
|
222
|
+
const allowed = report.trustedDockerPolicy.filter((item) => item.allowed).length;
|
|
223
|
+
console.log(`trustedDockerPolicy=${allowed}/${report.trustedDockerPolicy.length} allowed when sandbox=docker-workspace packageInstalls=allow`);
|
|
224
|
+
}
|
|
187
225
|
if (report.actionableSetup.length > 0) {
|
|
188
226
|
console.log("setup:");
|
|
189
227
|
for (const item of report.actionableSetup) {
|
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ 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";
|
|
7
8
|
import {
|
|
8
9
|
doctorReport,
|
|
9
10
|
initProject,
|
|
@@ -41,6 +42,7 @@ export function parseArgs(argv) {
|
|
|
41
42
|
allowShellTool: undefined,
|
|
42
43
|
allowFileTools: undefined,
|
|
43
44
|
allowWrapperTools: undefined,
|
|
45
|
+
allowDestructive: undefined,
|
|
44
46
|
preferredWrapper: "",
|
|
45
47
|
taskProfile: "",
|
|
46
48
|
useDockerSandbox: undefined,
|
|
@@ -51,6 +53,7 @@ export function parseArgs(argv) {
|
|
|
51
53
|
sandboxStatus: false,
|
|
52
54
|
sandboxPreflight: false,
|
|
53
55
|
web: false,
|
|
56
|
+
interactive: false,
|
|
54
57
|
port: "",
|
|
55
58
|
host: "",
|
|
56
59
|
listProfiles: false,
|
|
@@ -63,6 +66,10 @@ export function parseArgs(argv) {
|
|
|
63
66
|
result.web = true;
|
|
64
67
|
continue;
|
|
65
68
|
}
|
|
69
|
+
if (arg === "chat" || arg === "interactive" || arg === "--chat" || arg === "--interactive") {
|
|
70
|
+
result.interactive = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
66
73
|
if (arg === "--port") {
|
|
67
74
|
result.port = readOption(argv, i);
|
|
68
75
|
i += 1;
|
|
@@ -132,6 +139,14 @@ export function parseArgs(argv) {
|
|
|
132
139
|
result.allowShellTool = true;
|
|
133
140
|
continue;
|
|
134
141
|
}
|
|
142
|
+
if (arg === "--no-shell") {
|
|
143
|
+
result.allowShellTool = false;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
|
|
147
|
+
result.allowDestructive = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
135
150
|
if (arg === "--allow-file-tools") {
|
|
136
151
|
result.allowFileTools = true;
|
|
137
152
|
continue;
|
|
@@ -192,6 +207,12 @@ export function parseArgs(argv) {
|
|
|
192
207
|
return result;
|
|
193
208
|
}
|
|
194
209
|
|
|
210
|
+
function printUsage() {
|
|
211
|
+
console.log(
|
|
212
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti [--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"'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
function printRoutes() {
|
|
196
217
|
const presets = getModelPresets();
|
|
197
218
|
for (const preset of Object.values(presets)) {
|
|
@@ -307,6 +328,11 @@ async function handleSessionsCommand(argv) {
|
|
|
307
328
|
}
|
|
308
329
|
|
|
309
330
|
export async function main(argv = process.argv.slice(2)) {
|
|
331
|
+
if (argv[0] === "--help" || argv[0] === "help" || argv[0] === "-h") {
|
|
332
|
+
printUsage();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
310
336
|
if (argv[0] === "--version" || argv[0] === "version" || argv[0] === "-v") {
|
|
311
337
|
console.log(packageJson.version);
|
|
312
338
|
return;
|
|
@@ -388,13 +414,21 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
388
414
|
console.error('Usage: aginti resume <session-id> "new prompt"');
|
|
389
415
|
process.exit(1);
|
|
390
416
|
}
|
|
391
|
-
const config = loadConfig(
|
|
417
|
+
const config = loadConfig(
|
|
418
|
+
{ ...parseArgs([prompt]), resume: sessionId, goal: prompt, allowShellTool: true, allowFileTools: true },
|
|
419
|
+
{ packageDir }
|
|
420
|
+
);
|
|
392
421
|
await runAgent(config);
|
|
393
422
|
return;
|
|
394
423
|
}
|
|
395
424
|
|
|
396
425
|
const args = parseArgs(argv);
|
|
397
426
|
|
|
427
|
+
if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
|
|
428
|
+
await startInteractiveCli(args, { packageDir, packageVersion: packageJson.version });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
398
432
|
if (args.web) {
|
|
399
433
|
if (args.port) process.env.PORT = String(args.port);
|
|
400
434
|
if (args.host) process.env.HOST = String(args.host);
|
|
@@ -428,12 +462,17 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
428
462
|
}
|
|
429
463
|
|
|
430
464
|
if (!args.goal && !args.resume) {
|
|
431
|
-
|
|
432
|
-
'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"'
|
|
433
|
-
);
|
|
465
|
+
printUsage();
|
|
434
466
|
process.exit(1);
|
|
435
467
|
}
|
|
436
468
|
|
|
437
|
-
const config = loadConfig(
|
|
469
|
+
const config = loadConfig(
|
|
470
|
+
{
|
|
471
|
+
...args,
|
|
472
|
+
allowShellTool: args.allowShellTool ?? true,
|
|
473
|
+
allowFileTools: args.allowFileTools ?? true,
|
|
474
|
+
},
|
|
475
|
+
{ packageDir }
|
|
476
|
+
);
|
|
438
477
|
await runAgent(config);
|
|
439
478
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -40,6 +40,11 @@ const TEST_PATTERNS = [
|
|
|
40
40
|
|
|
41
41
|
const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
|
|
42
42
|
|
|
43
|
+
const NETWORK_FETCH_PATTERNS = [
|
|
44
|
+
/^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
|
|
45
|
+
/^wget\s+(?:-[A-Za-z0-9]*\s+)*(?:-O\s+[-\w./]+\s+)?https?:\/\/\S+$/,
|
|
46
|
+
];
|
|
47
|
+
|
|
43
48
|
const TOOLCHAIN_PATTERNS = [
|
|
44
49
|
/^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
|
|
45
50
|
/^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
|
|
@@ -48,12 +53,24 @@ const TOOLCHAIN_PATTERNS = [
|
|
|
48
53
|
|
|
49
54
|
const PACKAGE_INSTALL_PATTERNS = [
|
|
50
55
|
/^npm\s+ci$/,
|
|
51
|
-
/^npm\s+install
|
|
56
|
+
/^npm\s+install(?:\s+[-@\w./:=]+)*$/,
|
|
52
57
|
/^pnpm\s+install$/,
|
|
58
|
+
/^pnpm\s+add(?:\s+[-@\w./:=]+)+$/,
|
|
53
59
|
/^yarn\s+install$/,
|
|
60
|
+
/^yarn\s+add(?:\s+[-@\w./:=]+)+$/,
|
|
54
61
|
/^python(?:3)?\s+-m\s+pip\s+install\s+-r\s+[-\w./]+$/,
|
|
62
|
+
/^python(?:3)?\s+-m\s+pip\s+install(?:\s+[-@\w./:=]+)+$/,
|
|
55
63
|
/^pip(?:3)?\s+install\s+-r\s+[-\w./]+$/,
|
|
64
|
+
/^pip(?:3)?\s+install(?:\s+[-@\w./:=]+)+$/,
|
|
65
|
+
/^uv\s+(sync|pip\s+install)(?:\s+[-@\w./:=]+)*$/,
|
|
56
66
|
/^conda\s+env\s+(create|update)\s+-f\s+[-\w./]+$/,
|
|
67
|
+
/^conda\s+install(?:\s+[-@\w./:=]+)+$/,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const SYSTEM_PACKAGE_INSTALL_PATTERNS = [
|
|
71
|
+
/^(?:sudo\s+)?apt(?:-get)?\s+update$/,
|
|
72
|
+
/^(?:sudo\s+)?apt(?:-get)?\s+install(?:\s+-y)?(?:\s+[-@\w.+:=]+)+$/,
|
|
73
|
+
/^apk\s+add(?:\s+--no-cache)?(?:\s+[-@\w.+:=]+)+$/,
|
|
57
74
|
];
|
|
58
75
|
|
|
59
76
|
const ENV_SETUP_PATTERNS = [
|
|
@@ -64,13 +81,10 @@ const ENV_SETUP_PATTERNS = [
|
|
|
64
81
|
|
|
65
82
|
const BLOCKED_SHELL_TOKENS = ["&&", "||", ";", "|", ">", "<", "$(", "`"];
|
|
66
83
|
const BLOCKED_WRITE_TOKENS = [
|
|
67
|
-
"sudo ",
|
|
68
84
|
" rm",
|
|
69
85
|
" mv",
|
|
70
|
-
" cp",
|
|
71
86
|
" chmod",
|
|
72
87
|
" chown",
|
|
73
|
-
" mkdir",
|
|
74
88
|
" rmdir",
|
|
75
89
|
" touch",
|
|
76
90
|
" tee",
|
|
@@ -83,8 +97,6 @@ const BLOCKED_WRITE_TOKENS = [
|
|
|
83
97
|
"git switch",
|
|
84
98
|
"git reset",
|
|
85
99
|
"git clean",
|
|
86
|
-
"curl ",
|
|
87
|
-
"wget ",
|
|
88
100
|
];
|
|
89
101
|
|
|
90
102
|
const ALWAYS_BLOCKED_PATTERNS = [
|
|
@@ -98,6 +110,13 @@ const ALWAYS_BLOCKED_PATTERNS = [
|
|
|
98
110
|
/DEEPSEEK_API_KEY\s*=/i,
|
|
99
111
|
];
|
|
100
112
|
|
|
113
|
+
const SENSITIVE_COMMAND_PATTERNS = [
|
|
114
|
+
/(^|[\s./])\.env(\s|$|[./])/i,
|
|
115
|
+
/(^|[\s./])\.npmrc(\s|$|[./])/i,
|
|
116
|
+
/^(env|printenv)(\s|$)/i,
|
|
117
|
+
/(api[_-]?key|auth[_-]?token|npm[_-]?token|_authToken|bearer\s+[A-Za-z0-9._-]+)/i,
|
|
118
|
+
];
|
|
119
|
+
|
|
101
120
|
function normalizePolicy(value, allowed, fallback) {
|
|
102
121
|
return allowed.includes(value) ? value : fallback;
|
|
103
122
|
}
|
|
@@ -130,10 +149,18 @@ function classifySimpleCommand(normalized) {
|
|
|
130
149
|
if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
131
150
|
return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
|
|
132
151
|
}
|
|
152
|
+
if (SENSITIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
153
|
+
return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
|
|
154
|
+
}
|
|
133
155
|
|
|
134
156
|
const lowered = ` ${normalized.toLowerCase()} `;
|
|
135
157
|
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
136
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
category: "general-shell",
|
|
160
|
+
needsNetwork: true,
|
|
161
|
+
writesWorkspace: true,
|
|
162
|
+
reason: `Command uses general shell syntax: ${normalized}`,
|
|
163
|
+
};
|
|
137
164
|
}
|
|
138
165
|
if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
|
|
139
166
|
const target = normalized.replace(/^mkdir\s+-p\s+/, "");
|
|
@@ -144,7 +171,12 @@ function classifySimpleCommand(normalized) {
|
|
|
144
171
|
return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
|
|
145
172
|
}
|
|
146
173
|
if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
|
|
147
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
category: "destructive",
|
|
176
|
+
needsNetwork: false,
|
|
177
|
+
writesWorkspace: true,
|
|
178
|
+
reason: `Command contains a write-capable or destructive token: ${normalized}`,
|
|
179
|
+
};
|
|
148
180
|
}
|
|
149
181
|
|
|
150
182
|
if (matchAny(READ_ONLY_PATTERNS, normalized)) {
|
|
@@ -156,6 +188,12 @@ function classifySimpleCommand(normalized) {
|
|
|
156
188
|
if (matchAny(TOOLCHAIN_PATTERNS, normalized)) {
|
|
157
189
|
return { category: "toolchain", needsNetwork: false, writesWorkspace: true };
|
|
158
190
|
}
|
|
191
|
+
if (matchAny(NETWORK_FETCH_PATTERNS, normalized)) {
|
|
192
|
+
return { category: "network-fetch", needsNetwork: true, writesWorkspace: /(\s-o\s|\s-O\s)/.test(normalized) };
|
|
193
|
+
}
|
|
194
|
+
if (matchAny(SYSTEM_PACKAGE_INSTALL_PATTERNS, normalized)) {
|
|
195
|
+
return { category: "system-package-install", needsNetwork: true, writesWorkspace: false, requiresDockerRoot: true };
|
|
196
|
+
}
|
|
159
197
|
if (matchAny(PACKAGE_INSTALL_PATTERNS, normalized)) {
|
|
160
198
|
return { category: "package-install", needsNetwork: true, writesWorkspace: true };
|
|
161
199
|
}
|
|
@@ -163,7 +201,12 @@ function classifySimpleCommand(normalized) {
|
|
|
163
201
|
return { category: "env-setup", needsNetwork: false, writesWorkspace: true };
|
|
164
202
|
}
|
|
165
203
|
|
|
166
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
category: "general-shell",
|
|
206
|
+
needsNetwork: false,
|
|
207
|
+
writesWorkspace: false,
|
|
208
|
+
reason: `Command is outside the narrow allowlist and requires a trusted shell policy: ${normalized}`,
|
|
209
|
+
};
|
|
167
210
|
}
|
|
168
211
|
|
|
169
212
|
function classifyCdCommand(normalized) {
|
|
@@ -190,6 +233,10 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
190
233
|
const classification = classifyCommand(command);
|
|
191
234
|
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
192
235
|
const packageInstallPolicy = normalizePackageInstallPolicy(config.packageInstallPolicy);
|
|
236
|
+
const dockerWorkspace = sandboxMode === "docker-workspace";
|
|
237
|
+
const packageInstallsAllowed = packageInstallPolicy === "allow";
|
|
238
|
+
const trustedDockerShell = dockerWorkspace && packageInstallsAllowed;
|
|
239
|
+
const trustedHostShell = sandboxMode === "host" && Boolean(config.allowDestructive);
|
|
193
240
|
|
|
194
241
|
if (classification.category === "blocked") {
|
|
195
242
|
return { allowed: false, ...classification, sandboxMode, packageInstallPolicy };
|
|
@@ -215,7 +262,11 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
215
262
|
};
|
|
216
263
|
}
|
|
217
264
|
|
|
218
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
classification.category === "package-install" ||
|
|
267
|
+
classification.category === "env-setup" ||
|
|
268
|
+
classification.category === "system-package-install"
|
|
269
|
+
) {
|
|
219
270
|
if (packageInstallPolicy !== "allow") {
|
|
220
271
|
return {
|
|
221
272
|
allowed: false,
|
|
@@ -230,17 +281,53 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
230
281
|
};
|
|
231
282
|
}
|
|
232
283
|
|
|
233
|
-
if (
|
|
284
|
+
if (classification.category === "system-package-install" && sandboxMode === "host" && !config.allowDestructive) {
|
|
234
285
|
return {
|
|
235
286
|
allowed: false,
|
|
236
287
|
category: classification.category,
|
|
237
|
-
reason: "
|
|
288
|
+
reason: "Host system package installs require Allow destructive actions. Prefer Docker workspace mode.",
|
|
238
289
|
sandboxMode,
|
|
239
290
|
packageInstallPolicy,
|
|
240
291
|
};
|
|
241
292
|
}
|
|
242
293
|
}
|
|
243
294
|
|
|
295
|
+
if (classification.category === "general-shell" && !trustedDockerShell && !trustedHostShell) {
|
|
296
|
+
return {
|
|
297
|
+
allowed: false,
|
|
298
|
+
...classification,
|
|
299
|
+
needsApproval: true,
|
|
300
|
+
reason:
|
|
301
|
+
sandboxMode === "host"
|
|
302
|
+
? "General shell commands on the host require Allow destructive actions. Prefer Docker workspace mode for broad shell access."
|
|
303
|
+
: "General Docker shell commands require Package installs = allow in docker-workspace mode.",
|
|
304
|
+
sandboxMode,
|
|
305
|
+
packageInstallPolicy,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (classification.category === "destructive" && !config.allowDestructive) {
|
|
310
|
+
return {
|
|
311
|
+
allowed: false,
|
|
312
|
+
...classification,
|
|
313
|
+
needsApproval: true,
|
|
314
|
+
reason: "Destructive shell commands require Allow destructive actions.",
|
|
315
|
+
sandboxMode,
|
|
316
|
+
packageInstallPolicy,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (classification.category === "network-fetch" && config.useDockerSandbox && !trustedDockerShell) {
|
|
321
|
+
return {
|
|
322
|
+
allowed: false,
|
|
323
|
+
...classification,
|
|
324
|
+
needsApproval: true,
|
|
325
|
+
reason: "Docker network commands require docker-workspace mode with Package installs = allow.",
|
|
326
|
+
sandboxMode,
|
|
327
|
+
packageInstallPolicy,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
244
331
|
if (classification.writesWorkspace && config.useDockerSandbox && sandboxMode !== "docker-workspace") {
|
|
245
332
|
return {
|
|
246
333
|
allowed: false,
|
|
@@ -254,6 +341,7 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
254
341
|
return {
|
|
255
342
|
allowed: true,
|
|
256
343
|
...classification,
|
|
344
|
+
requiresDockerRoot: Boolean(classification.requiresDockerRoot && config.useDockerSandbox),
|
|
257
345
|
sandboxMode,
|
|
258
346
|
packageInstallPolicy,
|
|
259
347
|
};
|
package/src/config.js
CHANGED
|
@@ -71,7 +71,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
71
71
|
? overrides.allowedDomains
|
|
72
72
|
: parseList(process.env.ALLOWED_DOMAINS),
|
|
73
73
|
allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
|
|
74
|
-
allowDestructive: parseBoolean(overrides.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
74
|
+
allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
75
75
|
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, false),
|
|
76
76
|
allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
|
|
77
77
|
allowWrapperTools: parseBoolean(
|
package/src/docker-sandbox.js
CHANGED
|
@@ -168,7 +168,7 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
|
|
|
168
168
|
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
169
169
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
170
170
|
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
171
|
-
const userArgs = uid && gid ? ["--user", `${uid}:${gid}`] : [];
|
|
171
|
+
const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
|
|
172
172
|
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
173
173
|
const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
|
|
174
174
|
const networkMode = policy.needsNetwork ? "bridge" : "none";
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
" /installs block|prompt|allow",
|
|
25
|
+
" /cwd <path> Change command workspace.",
|
|
26
|
+
" /exit Quit.",
|
|
27
|
+
"",
|
|
28
|
+
"Type a normal request to run the agent. Example: write a Python CLI app with tests",
|
|
29
|
+
].join("\n")
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printStatus(state) {
|
|
34
|
+
console.log(`project=${process.cwd()}`);
|
|
35
|
+
console.log(`cwd=${state.commandCwd || process.cwd()}`);
|
|
36
|
+
console.log(`session=${state.sessionId || "new"}`);
|
|
37
|
+
console.log(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
|
|
38
|
+
console.log(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
|
|
39
|
+
console.log(
|
|
40
|
+
`shell=${state.allowShellTool} files=${state.allowFileTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
|
|
41
|
+
);
|
|
42
|
+
if (state.sandboxMode !== "host") {
|
|
43
|
+
console.log(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createState(args = {}) {
|
|
48
|
+
return {
|
|
49
|
+
provider: args.provider || "",
|
|
50
|
+
model: args.model || "",
|
|
51
|
+
routingMode: args.routingMode || "smart",
|
|
52
|
+
commandCwd: args.commandCwd || process.cwd(),
|
|
53
|
+
sandboxMode: normalizeSandboxMode(args.sandboxMode || "host"),
|
|
54
|
+
packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "prompt"),
|
|
55
|
+
allowShellTool: args.allowShellTool ?? true,
|
|
56
|
+
allowFileTools: args.allowFileTools ?? true,
|
|
57
|
+
allowWrapperTools: args.allowWrapperTools ?? false,
|
|
58
|
+
allowDestructive: args.allowDestructive ?? false,
|
|
59
|
+
preferredWrapper: args.preferredWrapper || "codex",
|
|
60
|
+
taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
|
|
61
|
+
headless: args.headless ?? false,
|
|
62
|
+
maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : 15,
|
|
63
|
+
sessionId: args.resume || "",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function handleCommand(line, state, packageDir) {
|
|
68
|
+
const [command, ...rest] = line.slice(1).trim().split(/\s+/);
|
|
69
|
+
const value = rest.join(" ").trim();
|
|
70
|
+
|
|
71
|
+
if (!command || command === "help" || command === "?") {
|
|
72
|
+
printHelp();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (command === "exit" || command === "quit" || command === "q") return false;
|
|
76
|
+
if (command === "status") {
|
|
77
|
+
printStatus(state);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (command === "new") {
|
|
81
|
+
state.sessionId = "";
|
|
82
|
+
console.log("Next message will start a new session.");
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (command === "resume") {
|
|
86
|
+
if (!value) console.log("Usage: /resume <session-id>");
|
|
87
|
+
else {
|
|
88
|
+
state.sessionId = value;
|
|
89
|
+
console.log(`Resuming ${state.sessionId}`);
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (command === "sessions") {
|
|
94
|
+
const sessions = await listProjectSessions(process.cwd(), 20);
|
|
95
|
+
if (sessions.length === 0) console.log("No project-local sessions found.");
|
|
96
|
+
else {
|
|
97
|
+
for (const session of sessions) {
|
|
98
|
+
const goal = session.goal ? ` ${session.goal.slice(0, 80)}` : "";
|
|
99
|
+
console.log(`${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (command === "profile") {
|
|
105
|
+
state.taskProfile = normalizeTaskProfile(value || "auto");
|
|
106
|
+
console.log(`profile=${state.taskProfile}`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (command === "routing") {
|
|
110
|
+
state.routingMode = value || "smart";
|
|
111
|
+
console.log(`routing=${state.routingMode}`);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (command === "provider") {
|
|
115
|
+
state.provider = value === "auto" ? "" : value;
|
|
116
|
+
console.log(`provider=${state.provider || "auto"}`);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (command === "model") {
|
|
120
|
+
state.model = value === "auto" ? "" : value;
|
|
121
|
+
console.log(`model=${state.model || "auto"}`);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (command === "installs") {
|
|
125
|
+
state.packageInstallPolicy = normalizePackageInstallPolicy(value || "prompt");
|
|
126
|
+
console.log(`installs=${state.packageInstallPolicy}`);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (command === "docker") {
|
|
130
|
+
if (value === "on") {
|
|
131
|
+
state.sandboxMode = "docker-workspace";
|
|
132
|
+
state.packageInstallPolicy = "allow";
|
|
133
|
+
console.log(`docker=on /workspace -> ${state.commandCwd || process.cwd()} installs=allow`);
|
|
134
|
+
} else if (value === "off") {
|
|
135
|
+
state.sandboxMode = "host";
|
|
136
|
+
state.packageInstallPolicy = "prompt";
|
|
137
|
+
console.log("docker=off sandbox=host installs=prompt");
|
|
138
|
+
} else {
|
|
139
|
+
console.log("Usage: /docker on OR /docker off");
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (command === "cwd") {
|
|
144
|
+
state.commandCwd = value || process.cwd();
|
|
145
|
+
console.log(`cwd=${state.commandCwd}`);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (command === "init") {
|
|
149
|
+
const result = await initProject(process.cwd());
|
|
150
|
+
console.log(`initialized project=${result.projectRoot}`);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (command === "web") {
|
|
154
|
+
const port = value || "3220";
|
|
155
|
+
console.log(`Run in another terminal: aginti web --port ${port}`);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`Unknown command: /${command}. Use /help.`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function runPrompt(prompt, state, packageDir) {
|
|
164
|
+
const config = loadConfig(
|
|
165
|
+
{
|
|
166
|
+
provider: state.provider,
|
|
167
|
+
model: state.model,
|
|
168
|
+
routingMode: state.routingMode,
|
|
169
|
+
commandCwd: state.commandCwd,
|
|
170
|
+
sandboxMode: state.sandboxMode,
|
|
171
|
+
packageInstallPolicy: state.packageInstallPolicy,
|
|
172
|
+
allowShellTool: state.allowShellTool,
|
|
173
|
+
allowFileTools: state.allowFileTools,
|
|
174
|
+
allowWrapperTools: state.allowWrapperTools,
|
|
175
|
+
allowDestructive: state.allowDestructive,
|
|
176
|
+
preferredWrapper: state.preferredWrapper,
|
|
177
|
+
taskProfile: state.taskProfile,
|
|
178
|
+
maxSteps: state.maxSteps,
|
|
179
|
+
headless: state.headless,
|
|
180
|
+
resume: state.sessionId,
|
|
181
|
+
goal: prompt,
|
|
182
|
+
},
|
|
183
|
+
{ packageDir, baseDir: process.cwd() }
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const result = await runAgent(config);
|
|
187
|
+
state.sessionId = result.sessionId || state.sessionId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
191
|
+
const state = createState(args);
|
|
192
|
+
const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
|
|
193
|
+
|
|
194
|
+
console.log(`AgInTiFlow ${packageVersion || ""}`.trim());
|
|
195
|
+
console.log(`Project: ${process.cwd()}`);
|
|
196
|
+
console.log("Interactive agent chat. Type /help for commands, /exit to quit.");
|
|
197
|
+
printStatus(state);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
while (true) {
|
|
201
|
+
let answer = "";
|
|
202
|
+
try {
|
|
203
|
+
answer = await rl.question("\naginti> ");
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error?.code === "ERR_USE_AFTER_CLOSE") break;
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
const line = answer.trim();
|
|
209
|
+
if (!line) continue;
|
|
210
|
+
if (line.startsWith("/")) {
|
|
211
|
+
const keepGoing = await handleCommand(line, state, packageDir);
|
|
212
|
+
if (!keepGoing) break;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await runPrompt(line, state, packageDir);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(`error: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
rl.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/model-client.js
CHANGED
|
@@ -300,7 +300,7 @@ export async function requestNextStep(client, config, messages) {
|
|
|
300
300
|
function: {
|
|
301
301
|
name: "run_command",
|
|
302
302
|
description:
|
|
303
|
-
"Run
|
|
303
|
+
"Run a terminal command in the configured working directory under the active shell policy. Secrets and npm publishing are always blocked; Docker workspace mode with approved package installs supports broader network/setup commands, while host destructive or privileged work requires explicit trust.",
|
|
304
304
|
parameters: {
|
|
305
305
|
type: "object",
|
|
306
306
|
properties: {
|
package/src/task-profiles.js
CHANGED
|
@@ -52,7 +52,7 @@ export const TASK_PROFILES = {
|
|
|
52
52
|
id: "website",
|
|
53
53
|
label: "Website testing",
|
|
54
54
|
prompt:
|
|
55
|
-
"For website-testing tasks, create or inspect the site, add a
|
|
55
|
+
"For website-testing tasks, create or inspect the site, add a local check when useful, and use the configured sandbox/package policy for dependencies.",
|
|
56
56
|
tools: ["files", "shell", "canvas", "sandbox"],
|
|
57
57
|
},
|
|
58
58
|
aaps: {
|
|
@@ -73,7 +73,7 @@ export const TASK_PROFILES = {
|
|
|
73
73
|
id: "maintenance",
|
|
74
74
|
label: "System maintenance",
|
|
75
75
|
prompt:
|
|
76
|
-
"For system maintenance, diagnose first,
|
|
76
|
+
"For system maintenance, diagnose first, use Docker for broad installs when available, and follow the configured trust/package policy for host-level changes.",
|
|
77
77
|
tools: ["shell", "sandbox", "files"],
|
|
78
78
|
},
|
|
79
79
|
};
|