@lazyingart/agintiflow 0.4.1 → 0.5.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 +25 -0
- package/docs/real-deepseek-capabilities.md +51 -0
- package/package.json +7 -3
- package/scripts/real-deepseek-capabilities.js +389 -0
- package/scripts/smoke-capabilities.js +74 -0
- package/scripts/smoke-web-api.js +8 -0
- package/src/agent-runner.js +31 -18
- package/src/capabilities.js +193 -0
- package/src/cli.js +32 -2
- package/src/command-policy.js +57 -3
- package/src/model-client.js +4 -2
- package/src/task-profiles.js +11 -4
- package/web.js +14 -0
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ Useful project commands:
|
|
|
66
66
|
```bash
|
|
67
67
|
aginti keys status
|
|
68
68
|
printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
|
|
69
|
+
aginti capabilities
|
|
70
|
+
aginti doctor --capabilities
|
|
69
71
|
aginti sessions list
|
|
70
72
|
aginti sessions show <session-id>
|
|
71
73
|
aginti resume <session-id> "continue with a short follow-up"
|
|
@@ -113,6 +115,29 @@ npm start -- --resume your-session-id
|
|
|
113
115
|
|
|
114
116
|
The package exposes both `aginti` and `aginti-cli`; they run the same CLI entrypoint.
|
|
115
117
|
|
|
118
|
+
## Capability Checks
|
|
119
|
+
|
|
120
|
+
Use the capability report to verify a project folder before running real agent work:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
aginti capabilities
|
|
124
|
+
aginti capabilities --json
|
|
125
|
+
aginti doctor --capabilities
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The report checks the project root, command cwd, shared `.sessions/`, provider-key presence, DeepSeek routes, guarded file and shell tools, Docker status, wrappers, task profiles, TeX, Node/npm, Python, R, conda, and maintenance command policy. It never prints API key or token values.
|
|
129
|
+
|
|
130
|
+
Live DeepSeek verification is opt-in because it spends provider credits:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
AGINTIFLOW_REAL_DEEPSEEK=1 \
|
|
134
|
+
AGINTIFLOW_REAL_WORKSPACE=/home/lachlan/ProjectsLFS/aginti-test \
|
|
135
|
+
AGINTIFLOW_REAL_WEB_BASE_URL=http://127.0.0.1:3220 \
|
|
136
|
+
npm run real:deepseek
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The live suite asks DeepSeek v4 flash/pro to create and improve a Node app, generate LaTeX/PDF artifacts when TeX exists, create a website-test sample, write Docker-safe maintenance plans, create an AAPS sample, and verify CLI/web session sharing. See [docs/real-deepseek-capabilities.md](docs/real-deepseek-capabilities.md).
|
|
140
|
+
|
|
116
141
|
## Web UI
|
|
117
142
|
|
|
118
143
|
The web app includes:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Real DeepSeek Capability Suite
|
|
2
|
+
|
|
3
|
+
AgInTiFlow keeps deterministic mock smoke tests for CI, but Round 9 added an opt-in live suite for validating DeepSeek v4 flash/pro behavior against a real project folder.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 22+
|
|
8
|
+
- A project folder initialized with `aginti init`
|
|
9
|
+
- `DEEPSEEK_API_KEY` in the environment or project-local `.aginti/.env`
|
|
10
|
+
- Optional web UI running from the same project folder
|
|
11
|
+
|
|
12
|
+
Never print or commit API keys. The suite reports only provider availability.
|
|
13
|
+
|
|
14
|
+
## Static Capability Report
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
aginti capabilities
|
|
18
|
+
aginti capabilities --json
|
|
19
|
+
aginti doctor --capabilities
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The report checks project root, command cwd, shared `.sessions/`, provider-key presence, DeepSeek routes, file/shell tools, Docker status, wrappers, task profiles, TeX, Node/npm, Python, R, conda, and maintenance command guardrails.
|
|
23
|
+
|
|
24
|
+
## Live DeepSeek Suite
|
|
25
|
+
|
|
26
|
+
Run from the source repository:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
AGINTIFLOW_REAL_DEEPSEEK=1 \
|
|
30
|
+
AGINTIFLOW_REAL_WORKSPACE=/home/lachlan/ProjectsLFS/aginti-test \
|
|
31
|
+
AGINTIFLOW_REAL_WEB_BASE_URL=http://127.0.0.1:3220 \
|
|
32
|
+
npm run real:deepseek
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Optional case filter:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
AGINTIFLOW_REAL_DEEPSEEK=1 AGINTIFLOW_REAL_CASES=flash,pro npm run real:deepseek
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What It Validates
|
|
42
|
+
|
|
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
|
+
- `pro`: DeepSeek v4 pro resumes/improves the app and expands tests.
|
|
45
|
+
- `latex`: creates `.tex` source and compiles only if a TeX toolchain is available; otherwise writes an honest setup artifact.
|
|
46
|
+
- `website`: creates a dependency-free website test sample and runs safe tests when dependencies are present.
|
|
47
|
+
- `maintenance`: creates dry-run maintenance scripts/plans under `maintenance/` and validates script syntax; no global installs.
|
|
48
|
+
- `aaps`: creates a project-local `.aaps` sample and notes for `@lazyingart/aaps` workflows without publishing.
|
|
49
|
+
- `web`: when `AGINTIFLOW_REAL_WEB_BASE_URL` is set, starts a web run and verifies CLI/web session sharing.
|
|
50
|
+
|
|
51
|
+
The suite writes a JSON summary with session IDs, generated files, routes, and failures. It is intentionally opt-in because it spends live model credits.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"public/",
|
|
41
41
|
"scripts/install-docker-ubuntu.sh",
|
|
42
42
|
"scripts/setup-agent-toolchain-docker.sh",
|
|
43
|
+
"scripts/real-deepseek-capabilities.js",
|
|
43
44
|
"scripts/smoke-coding-tools.js",
|
|
45
|
+
"scripts/smoke-capabilities.js",
|
|
44
46
|
"scripts/smoke-toolchain-docker.js",
|
|
45
47
|
"scripts/smoke-web-api.js",
|
|
46
48
|
"src/",
|
|
@@ -59,8 +61,10 @@
|
|
|
59
61
|
"smoke:coding-tools": "node scripts/smoke-coding-tools.js",
|
|
60
62
|
"smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
|
|
61
63
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
+
"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",
|
|
66
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
67
|
+
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
64
68
|
},
|
|
65
69
|
"dependencies": {
|
|
66
70
|
"express": "^5.1.0",
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { runAgent } from "../src/agent-runner.js";
|
|
8
|
+
import { buildCapabilityReport } from "../src/capabilities.js";
|
|
9
|
+
import { resolveRuntimeConfig } from "../src/config.js";
|
|
10
|
+
import { initProject, listProjectSessions, providerKeyStatus, showProjectSession } from "../src/project.js";
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
15
|
+
const projectRoot = path.resolve(process.env.AGINTIFLOW_REAL_WORKSPACE || "/home/lachlan/ProjectsLFS/aginti-test");
|
|
16
|
+
const webBaseUrl = String(process.env.AGINTIFLOW_REAL_WEB_BASE_URL || "").replace(/\/$/, "");
|
|
17
|
+
const enabled = process.env.AGINTIFLOW_REAL_DEEPSEEK === "1";
|
|
18
|
+
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
19
|
+
function isJavaScriptTest(file) {
|
|
20
|
+
const base = path.basename(file);
|
|
21
|
+
return /^(test|.+\.(test|spec))\.[mc]?js$/.test(base) || (file.includes("/test/") && /\.[mc]?js$/.test(base));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function selectedCases() {
|
|
25
|
+
const raw = process.env.AGINTIFLOW_REAL_CASES || "flash,pro,latex,website,maintenance,aaps";
|
|
26
|
+
return new Set(raw.split(",").map((item) => item.trim()).filter(Boolean));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function baseConfig(goal, overrides = {}) {
|
|
30
|
+
return resolveRuntimeConfig(
|
|
31
|
+
{
|
|
32
|
+
provider: "deepseek",
|
|
33
|
+
routingMode: overrides.routingMode || "smart",
|
|
34
|
+
model: overrides.model || "",
|
|
35
|
+
goal,
|
|
36
|
+
commandCwd: projectRoot,
|
|
37
|
+
maxSteps: overrides.maxSteps || 12,
|
|
38
|
+
sandboxMode: overrides.sandboxMode || "host",
|
|
39
|
+
packageInstallPolicy: overrides.packageInstallPolicy || "block",
|
|
40
|
+
allowShellTool: overrides.allowShellTool ?? true,
|
|
41
|
+
allowFileTools: true,
|
|
42
|
+
taskProfile: overrides.taskProfile || "auto",
|
|
43
|
+
resume: overrides.resume || "",
|
|
44
|
+
sessionId: overrides.sessionId || "",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
baseDir: projectRoot,
|
|
48
|
+
packageDir: repoRoot,
|
|
49
|
+
provider: "deepseek",
|
|
50
|
+
routingMode: overrides.routingMode || "smart",
|
|
51
|
+
model: overrides.model || "",
|
|
52
|
+
commandCwd: projectRoot,
|
|
53
|
+
maxSteps: overrides.maxSteps || 12,
|
|
54
|
+
sandboxMode: overrides.sandboxMode || "host",
|
|
55
|
+
packageInstallPolicy: overrides.packageInstallPolicy || "block",
|
|
56
|
+
allowShellTool: overrides.allowShellTool ?? true,
|
|
57
|
+
allowFileTools: true,
|
|
58
|
+
taskProfile: overrides.taskProfile || "auto",
|
|
59
|
+
resume: overrides.resume || "",
|
|
60
|
+
sessionId: overrides.resume ? "" : overrides.sessionId,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runCliCase(name, goal, overrides = {}) {
|
|
66
|
+
const config = baseConfig(goal, {
|
|
67
|
+
...overrides,
|
|
68
|
+
sessionId: overrides.sessionId || `round9-${name}-${stamp}`,
|
|
69
|
+
});
|
|
70
|
+
const startedAt = new Date().toISOString();
|
|
71
|
+
const result = await runAgent(config);
|
|
72
|
+
const sessionId = result.sessionId || config.sessionId || config.resume;
|
|
73
|
+
const session = await showProjectSession(projectRoot, sessionId).catch(() => null);
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
channel: "cli",
|
|
77
|
+
sessionId,
|
|
78
|
+
provider: config.provider,
|
|
79
|
+
model: config.model,
|
|
80
|
+
routingMode: config.routingMode,
|
|
81
|
+
taskProfile: config.taskProfile,
|
|
82
|
+
startedAt,
|
|
83
|
+
result: result.result || "",
|
|
84
|
+
stopped: Boolean(result.stopped),
|
|
85
|
+
events: session?.events?.length || 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchJson(url, options = {}) {
|
|
90
|
+
const response = await fetch(url, options);
|
|
91
|
+
const data = await response.json().catch(() => ({}));
|
|
92
|
+
if (!response.ok) throw new Error(`${url} failed ${response.status}: ${data.error || response.statusText}`);
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function waitForWebRun(sessionId) {
|
|
97
|
+
const deadline = Date.now() + 180000;
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
const run = await fetchJson(`${webBaseUrl}/api/runs/${encodeURIComponent(sessionId)}`);
|
|
100
|
+
if (run.status === "finished" || run.status === "failed") return run;
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Web run timed out: ${sessionId}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runWebCase(name, goal, overrides = {}) {
|
|
107
|
+
if (!webBaseUrl) return null;
|
|
108
|
+
const started = await fetchJson(`${webBaseUrl}/api/runs`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
provider: "deepseek",
|
|
113
|
+
routingMode: overrides.routingMode || "smart",
|
|
114
|
+
model: overrides.model || "",
|
|
115
|
+
goal,
|
|
116
|
+
commandCwd: projectRoot,
|
|
117
|
+
sandboxMode: overrides.sandboxMode || "host",
|
|
118
|
+
packageInstallPolicy: overrides.packageInstallPolicy || "block",
|
|
119
|
+
allowShellTool: overrides.allowShellTool ?? true,
|
|
120
|
+
allowFileTools: true,
|
|
121
|
+
maxSteps: overrides.maxSteps || 12,
|
|
122
|
+
taskProfile: overrides.taskProfile || "auto",
|
|
123
|
+
headless: true,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
const run = await waitForWebRun(started.sessionId);
|
|
127
|
+
return {
|
|
128
|
+
name,
|
|
129
|
+
channel: "web",
|
|
130
|
+
sessionId: started.sessionId,
|
|
131
|
+
provider: run.provider,
|
|
132
|
+
model: run.model,
|
|
133
|
+
status: run.status,
|
|
134
|
+
result: run.result || "",
|
|
135
|
+
error: run.error || "",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function assertFile(relativePath) {
|
|
140
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
141
|
+
const stat = await fs.stat(absolutePath).catch(() => null);
|
|
142
|
+
return {
|
|
143
|
+
path: relativePath,
|
|
144
|
+
exists: Boolean(stat),
|
|
145
|
+
size: stat?.size || 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function listFilesRecursive(relativeDir) {
|
|
150
|
+
const root = path.join(projectRoot, relativeDir);
|
|
151
|
+
const entries = [];
|
|
152
|
+
async function visit(currentAbs, currentRel) {
|
|
153
|
+
const children = await fs.readdir(currentAbs, { withFileTypes: true }).catch(() => []);
|
|
154
|
+
for (const child of children) {
|
|
155
|
+
const childAbs = path.join(currentAbs, child.name);
|
|
156
|
+
const childRel = path.posix.join(currentRel.split(path.sep).join(path.posix.sep), child.name);
|
|
157
|
+
if (child.isDirectory()) await visit(childAbs, childRel);
|
|
158
|
+
else if (child.isFile()) entries.push(childRel);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await visit(root, relativeDir);
|
|
162
|
+
return entries;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runLocalCheck(command, args, cwdRel = ".") {
|
|
166
|
+
try {
|
|
167
|
+
const result = await execFileAsync(command, args, {
|
|
168
|
+
cwd: path.join(projectRoot, cwdRel),
|
|
169
|
+
timeout: 90000,
|
|
170
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
171
|
+
env: {
|
|
172
|
+
...process.env,
|
|
173
|
+
npm_config_loglevel: "warn",
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
command: [command, ...args].join(" "),
|
|
179
|
+
stdout: String(result.stdout || "").slice(0, 1600),
|
|
180
|
+
stderr: String(result.stderr || "").slice(0, 1600),
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
command: [command, ...args].join(" "),
|
|
186
|
+
stdout: String(error.stdout || "").slice(0, 1600),
|
|
187
|
+
stderr: String(error.stderr || error.message || "").slice(0, 1600),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addCheck(report, name, ok, details = {}) {
|
|
193
|
+
report.checks.push({ name, ok: Boolean(ok), ...details });
|
|
194
|
+
if (!ok) report.ok = false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function addStoppedWarning(report, run) {
|
|
198
|
+
if (run?.stopped) {
|
|
199
|
+
report.warnings.push(`${run.name} reached max steps before finish(); artifacts were validated separately.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!enabled) {
|
|
204
|
+
console.log(
|
|
205
|
+
JSON.stringify(
|
|
206
|
+
{
|
|
207
|
+
ok: true,
|
|
208
|
+
skipped: true,
|
|
209
|
+
reason: "Set AGINTIFLOW_REAL_DEEPSEEK=1 to run live DeepSeek capability checks.",
|
|
210
|
+
},
|
|
211
|
+
null,
|
|
212
|
+
2
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await initProject(projectRoot);
|
|
219
|
+
const keyStatus = providerKeyStatus(projectRoot);
|
|
220
|
+
if (!keyStatus.deepseek) {
|
|
221
|
+
console.log(
|
|
222
|
+
JSON.stringify(
|
|
223
|
+
{
|
|
224
|
+
ok: false,
|
|
225
|
+
skipped: true,
|
|
226
|
+
reason: "DeepSeek key is not available by env or project-local .aginti/.env.",
|
|
227
|
+
},
|
|
228
|
+
null,
|
|
229
|
+
2
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
process.exit(0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const cases = selectedCases();
|
|
236
|
+
const appDir = `round9/deepseek-app-${stamp}`;
|
|
237
|
+
const report = {
|
|
238
|
+
ok: true,
|
|
239
|
+
projectRoot,
|
|
240
|
+
webBaseUrl: webBaseUrl || "",
|
|
241
|
+
stamp,
|
|
242
|
+
capabilities: await buildCapabilityReport(
|
|
243
|
+
projectRoot,
|
|
244
|
+
packageJson.version,
|
|
245
|
+
baseConfig("capability report", { allowShellTool: true, allowFileTools: true })
|
|
246
|
+
),
|
|
247
|
+
runs: [],
|
|
248
|
+
files: [],
|
|
249
|
+
checks: [],
|
|
250
|
+
warnings: [],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (cases.has("flash")) {
|
|
254
|
+
const run = await runCliCase(
|
|
255
|
+
"flash-node-app",
|
|
256
|
+
`Create a small dependency-free Node and HTML app with tests under ${appDir}. Run safe checks if you can.`,
|
|
257
|
+
{ routingMode: "fast", taskProfile: "node", maxSteps: 24 }
|
|
258
|
+
);
|
|
259
|
+
report.runs.push(run);
|
|
260
|
+
addStoppedWarning(report, run);
|
|
261
|
+
const files = await listFilesRecursive(appDir);
|
|
262
|
+
const testFile = files.find(isJavaScriptTest);
|
|
263
|
+
const appHasCode = files.some((file) => /\.(js|mjs)$/.test(file));
|
|
264
|
+
const appHasHtml = files.some((file) => /\.html$/.test(file));
|
|
265
|
+
const testResult = testFile ? await runLocalCheck("node", ["--test", testFile]) : { ok: false, command: "node --test <missing>" };
|
|
266
|
+
report.files.push(await assertFile(testFile || `${appDir}/test/app.test.js`));
|
|
267
|
+
addCheck(report, "flash-node-app-files", appHasCode && appHasHtml && Boolean(testFile), { files });
|
|
268
|
+
addCheck(report, "flash-node-app-tests", testResult.ok, testResult);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (cases.has("pro")) {
|
|
272
|
+
const previous = report.runs.find((run) => run.name === "flash-node-app");
|
|
273
|
+
const run = await runCliCase(
|
|
274
|
+
"pro-improve-app",
|
|
275
|
+
`Improve the app in ${appDir} with a useful new feature and matching tests. Run safe checks if you can.`,
|
|
276
|
+
{
|
|
277
|
+
routingMode: "complex",
|
|
278
|
+
taskProfile: "code",
|
|
279
|
+
maxSteps: 24,
|
|
280
|
+
resume: previous?.sessionId || "",
|
|
281
|
+
sessionId: previous ? "" : `round9-pro-improve-${stamp}`,
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
report.runs.push(run);
|
|
285
|
+
addStoppedWarning(report, run);
|
|
286
|
+
const files = await listFilesRecursive(appDir);
|
|
287
|
+
const testFile = files.find(isJavaScriptTest);
|
|
288
|
+
const testResult = testFile ? await runLocalCheck("node", ["--test", testFile]) : { ok: false, command: "node --test <missing>" };
|
|
289
|
+
addCheck(report, "pro-improve-app-tests", testResult.ok, testResult);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (cases.has("latex")) {
|
|
293
|
+
const texDir = `round9/latex-${stamp}`;
|
|
294
|
+
const run = await runCliCase(
|
|
295
|
+
"latex-report",
|
|
296
|
+
`Write a small LaTeX note under ${texDir} about AgInTiFlow capability testing. Compile it if TeX is available; otherwise create an honest setup note.`,
|
|
297
|
+
{ routingMode: "complex", taskProfile: "latex", maxSteps: 16 }
|
|
298
|
+
);
|
|
299
|
+
report.runs.push(run);
|
|
300
|
+
addStoppedWarning(report, run);
|
|
301
|
+
const files = await listFilesRecursive(texDir);
|
|
302
|
+
report.files.push(await assertFile(files.find((file) => file.endsWith(".tex")) || `${texDir}/note.tex`));
|
|
303
|
+
addCheck(report, "latex-source", files.some((file) => file.endsWith(".tex")), { files });
|
|
304
|
+
addCheck(report, "latex-pdf-or-setup", files.some((file) => file.endsWith(".pdf") || /setup|readme/i.test(file)), { files });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (cases.has("website")) {
|
|
308
|
+
const websiteDir = `round9/website-test-${stamp}`;
|
|
309
|
+
const run = await runCliCase(
|
|
310
|
+
"website-test",
|
|
311
|
+
`Create a small website under ${websiteDir} with a simple local test or check file. Run safe checks if you can without installing packages.`,
|
|
312
|
+
{ routingMode: "fast", taskProfile: "website", maxSteps: 24 }
|
|
313
|
+
);
|
|
314
|
+
report.runs.push(run);
|
|
315
|
+
addStoppedWarning(report, run);
|
|
316
|
+
const files = await listFilesRecursive(websiteDir);
|
|
317
|
+
const nodeTest = files.find(isJavaScriptTest);
|
|
318
|
+
const pythonTest = files.find((file) => /(^|\/)(test_.*|.*_test)\.py$/.test(path.basename(file)));
|
|
319
|
+
const shellTest = files.find((file) => /^test[-_\w]*\.sh$/.test(path.basename(file)));
|
|
320
|
+
const testResult = nodeTest
|
|
321
|
+
? await runLocalCheck("node", ["--test", nodeTest])
|
|
322
|
+
: pythonTest
|
|
323
|
+
? await runLocalCheck("python3", [pythonTest])
|
|
324
|
+
: shellTest
|
|
325
|
+
? await runLocalCheck("bash", ["-n", shellTest])
|
|
326
|
+
: { ok: false, command: "<missing website test>" };
|
|
327
|
+
report.files.push(await assertFile(nodeTest || pythonTest || shellTest || `${websiteDir}/test/website.test.js`));
|
|
328
|
+
addCheck(report, "website-test-files", files.some((file) => file.endsWith(".html")) && Boolean(nodeTest || pythonTest || shellTest), { files });
|
|
329
|
+
addCheck(report, "website-test-runs", testResult.ok, testResult);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (cases.has("maintenance")) {
|
|
333
|
+
const maintenanceDir = `maintenance/round9-${stamp}`;
|
|
334
|
+
const run = await runCliCase(
|
|
335
|
+
"maintenance-plan",
|
|
336
|
+
`Create project-local dry-run maintenance plans and scripts under ${maintenanceDir} for Miniforge/conda, R, Python tooling, CmdStan/CmdStanR, and PyStan. Validate scripts safely if you can.`,
|
|
337
|
+
{
|
|
338
|
+
routingMode: "complex",
|
|
339
|
+
taskProfile: "maintenance",
|
|
340
|
+
sandboxMode: "docker-workspace",
|
|
341
|
+
packageInstallPolicy: "block",
|
|
342
|
+
maxSteps: 24,
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
report.runs.push(run);
|
|
346
|
+
addStoppedWarning(report, run);
|
|
347
|
+
const files = await listFilesRecursive(maintenanceDir);
|
|
348
|
+
const shellScripts = files.filter((file) => file.endsWith(".sh"));
|
|
349
|
+
const syntaxResults = [];
|
|
350
|
+
for (const script of shellScripts) syntaxResults.push(await runLocalCheck("bash", ["-n", script]));
|
|
351
|
+
report.files.push(await assertFile(files.find((file) => /\.md$/i.test(file)) || `${maintenanceDir}/README.md`));
|
|
352
|
+
addCheck(report, "maintenance-plan-files", files.some((file) => /\.md$/i.test(file)) && shellScripts.length > 0, { files });
|
|
353
|
+
addCheck(report, "maintenance-shell-syntax", syntaxResults.every((item) => item.ok), { syntaxResults });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (cases.has("aaps")) {
|
|
357
|
+
const run = await runCliCase(
|
|
358
|
+
"aaps-sample",
|
|
359
|
+
"Create a small project-local AAPS sample and notes for @lazyingart/aaps workflows.",
|
|
360
|
+
{ routingMode: "fast", taskProfile: "aaps", maxSteps: 14 }
|
|
361
|
+
);
|
|
362
|
+
report.runs.push(run);
|
|
363
|
+
addStoppedWarning(report, run);
|
|
364
|
+
const files = [
|
|
365
|
+
...(await listFilesRecursive(".aaps")),
|
|
366
|
+
...(await listFilesRecursive("aaps-sample")),
|
|
367
|
+
];
|
|
368
|
+
const aapsFile = files.find((file) => file.includes(".aaps/") || file.endsWith(".aaps") || file.endsWith(".json"));
|
|
369
|
+
report.files.push(await assertFile(aapsFile || ".aaps/round9-sample.json"));
|
|
370
|
+
addCheck(report, "aaps-sample-files", Boolean(aapsFile), { files });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (webBaseUrl) {
|
|
374
|
+
const run = await runWebCase(
|
|
375
|
+
"web-sync-real",
|
|
376
|
+
`Create notes/web-real-${stamp}.md with a short note that this web run shares the project session folder.`,
|
|
377
|
+
{ routingMode: "fast", taskProfile: "code", maxSteps: 10 }
|
|
378
|
+
);
|
|
379
|
+
report.runs.push(run);
|
|
380
|
+
report.files.push(await assertFile(`notes/web-real-${stamp}.md`));
|
|
381
|
+
addCheck(report, "web-sync-file", (await assertFile(`notes/web-real-${stamp}.md`)).exists, {
|
|
382
|
+
sessionId: run?.sessionId,
|
|
383
|
+
status: run?.status,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
report.sessions = await listProjectSessions(projectRoot, 20);
|
|
388
|
+
console.log(JSON.stringify(report, null, 2));
|
|
389
|
+
if (!report.ok) process.exitCode = 1;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } 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 { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-capabilities-"));
|
|
12
|
+
|
|
13
|
+
function assert(condition, message) {
|
|
14
|
+
if (!condition) throw new Error(message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function runCli(args) {
|
|
18
|
+
const result = await execFileAsync(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), ...args], {
|
|
19
|
+
cwd: tempRoot,
|
|
20
|
+
timeout: 20000,
|
|
21
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
22
|
+
env: {
|
|
23
|
+
...process.env,
|
|
24
|
+
AGINTIFLOW_RUNTIME_DIR: "",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return result.stdout;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await runCli(["init"]);
|
|
32
|
+
const capabilities = JSON.parse(await runCli(["capabilities", "--json"]));
|
|
33
|
+
assert(capabilities.project.root === tempRoot, "capabilities did not use cwd as project root");
|
|
34
|
+
assert(capabilities.project.commandCwd === tempRoot, "capabilities did not default commandCwd to project root");
|
|
35
|
+
assert(capabilities.project.sharedSessionFolder, "capabilities did not report shared session folder");
|
|
36
|
+
assert(capabilities.keys?.mock === true, "capabilities did not report mock availability");
|
|
37
|
+
assert(
|
|
38
|
+
capabilities.checks.some((check) => check.name === "npm-prefix-test-policy" && check.ok),
|
|
39
|
+
"npm --prefix test policy is not allowed"
|
|
40
|
+
);
|
|
41
|
+
assert(
|
|
42
|
+
capabilities.checks.some((check) => check.name === "cd-npm-test-policy" && check.ok),
|
|
43
|
+
"cd <dir> && npm test policy is not allowed"
|
|
44
|
+
);
|
|
45
|
+
assert(
|
|
46
|
+
capabilities.checks.some((check) => check.name === "mkdir-policy" && check.ok),
|
|
47
|
+
"safe mkdir -p policy is not allowed"
|
|
48
|
+
);
|
|
49
|
+
assert(
|
|
50
|
+
capabilities.checks.some((check) => check.name === "bash-syntax-policy" && check.ok),
|
|
51
|
+
"bash -n maintenance script policy is not allowed"
|
|
52
|
+
);
|
|
53
|
+
assert(
|
|
54
|
+
capabilities.maintenancePolicy.some((check) => check.command.startsWith("sudo") && !check.allowed),
|
|
55
|
+
"sudo maintenance command was not blocked"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
|
|
59
|
+
assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
ok: true,
|
|
65
|
+
projectRoot: tempRoot,
|
|
66
|
+
checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy"],
|
|
67
|
+
},
|
|
68
|
+
null,
|
|
69
|
+
2
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
} finally {
|
|
73
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
74
|
+
}
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -83,6 +83,13 @@ try {
|
|
|
83
83
|
const keyStatus = await fetchJson("/api/keys/status");
|
|
84
84
|
if (typeof keyStatus.keyStatus?.deepseek !== "boolean") throw new Error("key status endpoint is invalid");
|
|
85
85
|
if ("localEnvPath" in keyStatus.keyStatus) throw new Error("key status leaked a local env path");
|
|
86
|
+
const capabilities = await fetchJson("/api/capabilities");
|
|
87
|
+
if (capabilities.project?.root !== runtimeDir || !Array.isArray(capabilities.checks)) {
|
|
88
|
+
throw new Error("capability endpoint returned an invalid project report");
|
|
89
|
+
}
|
|
90
|
+
if (!capabilities.checks.some((check) => check.name === "npm-prefix-test-policy")) {
|
|
91
|
+
throw new Error("capability endpoint did not include command policy checks");
|
|
92
|
+
}
|
|
86
93
|
const savedKey = await fetchJson("/api/keys/deepseek", {
|
|
87
94
|
method: "POST",
|
|
88
95
|
headers: { "Content-Type": "application/json" },
|
|
@@ -231,6 +238,7 @@ try {
|
|
|
231
238
|
endpoints: [
|
|
232
239
|
"/api/config",
|
|
233
240
|
"/api/keys/status",
|
|
241
|
+
"/api/capabilities",
|
|
234
242
|
"POST /api/keys/:provider",
|
|
235
243
|
"/api/sandbox/status",
|
|
236
244
|
"/api/sandbox/preflight",
|
package/src/agent-runner.js
CHANGED
|
@@ -156,7 +156,10 @@ function createInitialState(config, sessionId) {
|
|
|
156
156
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
157
157
|
"A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
|
|
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
|
+
"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, prefer project-local dry-run plans/scripts unless the configured policy explicitly allows stronger actions.",
|
|
162
|
+
"When the requested outcome is complete and a useful check has passed or been honestly skipped, stop and call finish.",
|
|
160
163
|
"When done, call finish with a concise result.",
|
|
161
164
|
].join(" "),
|
|
162
165
|
},
|
|
@@ -178,7 +181,8 @@ function createInitialState(config, sessionId) {
|
|
|
178
181
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
179
182
|
"Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
|
|
180
183
|
"Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
|
|
181
|
-
"
|
|
184
|
+
"Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
|
|
185
|
+
"Keep environment or system-maintenance actions project-local and reversible unless policy explicitly permits stronger actions.",
|
|
182
186
|
]
|
|
183
187
|
.filter(Boolean)
|
|
184
188
|
.join("\n"),
|
|
@@ -369,22 +373,31 @@ function sanitizeToolResult(result) {
|
|
|
369
373
|
}
|
|
370
374
|
|
|
371
375
|
async function runShellCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
376
|
+
try {
|
|
377
|
+
const result = config.useDockerSandbox
|
|
378
|
+
? await runDockerSandboxCommand(command, config, policy)
|
|
379
|
+
: await exec(command, {
|
|
380
|
+
cwd: config.commandCwd,
|
|
381
|
+
timeout: 30000,
|
|
382
|
+
maxBuffer: 200 * 1024,
|
|
383
|
+
shell: "/bin/bash",
|
|
384
|
+
env: safeExecutionEnv(),
|
|
385
|
+
});
|
|
383
386
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
exitCode: 0,
|
|
390
|
+
stdout: redactSensitiveText(result.stdout).trim().slice(0, 8000),
|
|
391
|
+
stderr: redactSensitiveText(result.stderr).trim().slice(0, 4000),
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
exitCode: Number.isInteger(error?.code) ? error.code : 1,
|
|
397
|
+
stdout: redactSensitiveText(String(error?.stdout || "")).trim().slice(0, 8000),
|
|
398
|
+
stderr: redactSensitiveText(String(error?.stderr || error?.message || "")).trim().slice(0, 4000),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
388
401
|
}
|
|
389
402
|
|
|
390
403
|
async function captureSyntheticSnapshot(store, step, config) {
|
|
@@ -565,7 +578,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
565
578
|
}
|
|
566
579
|
const commandResult = await runShellCommand(String(args.command), config, policy);
|
|
567
580
|
const result = {
|
|
568
|
-
ok:
|
|
581
|
+
ok: commandResult.ok !== false,
|
|
569
582
|
toolName: "run_command",
|
|
570
583
|
args: safeArgs,
|
|
571
584
|
sandbox: config.useDockerSandbox ? "docker" : "host",
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { classifyCommand, evaluateCommandPolicy } from "./command-policy.js";
|
|
5
|
+
import { getDockerSandboxStatus } from "./docker-sandbox.js";
|
|
6
|
+
import { getModelPresets } from "./model-routing.js";
|
|
7
|
+
import { listProjectSessions, projectPaths, providerKeyStatus } from "./project.js";
|
|
8
|
+
import { listTaskProfiles } from "./task-profiles.js";
|
|
9
|
+
import { listAgentWrappers } from "./tool-wrappers.js";
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
async function commandAvailable(command, args = ["--version"], timeout = 2500) {
|
|
14
|
+
try {
|
|
15
|
+
const result = await execFileAsync(command, args, {
|
|
16
|
+
timeout,
|
|
17
|
+
maxBuffer: 120 * 1024,
|
|
18
|
+
env: {
|
|
19
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
20
|
+
HOME: process.env.HOME || "",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter(Boolean)[0] || "available";
|
|
27
|
+
return { available: true, version: output.slice(0, 160) };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
available: false,
|
|
31
|
+
version: "",
|
|
32
|
+
hint: error?.code === "ENOENT" ? `${command} was not found on PATH.` : `${command} check failed.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function capability(name, ok, details = {}) {
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
ok: Boolean(ok),
|
|
41
|
+
...details,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function maintenancePolicyChecks(config) {
|
|
46
|
+
const sampleCommands = [
|
|
47
|
+
"sudo apt install r-base",
|
|
48
|
+
"curl https://example.com/install.sh",
|
|
49
|
+
"npm install",
|
|
50
|
+
"bash -n maintenance/setup-conda.sh",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return sampleCommands.map((command) => {
|
|
54
|
+
const policy = evaluateCommandPolicy(command, config);
|
|
55
|
+
return {
|
|
56
|
+
command,
|
|
57
|
+
allowed: Boolean(policy.allowed),
|
|
58
|
+
category: policy.category || classifyCommand(command).category,
|
|
59
|
+
reason: policy.reason || "",
|
|
60
|
+
needsApproval: Boolean(policy.needsApproval),
|
|
61
|
+
sandboxMode: policy.sandboxMode,
|
|
62
|
+
packageInstallPolicy: policy.packageInstallPolicy,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function buildCapabilityReport(projectRoot, packageVersion, config) {
|
|
68
|
+
const paths = projectPaths(projectRoot);
|
|
69
|
+
const keyStatus = providerKeyStatus(projectRoot);
|
|
70
|
+
const [node, npm, python, conda, r, pdflatex, latexmk, dockerStatus, sessions] = await Promise.all([
|
|
71
|
+
commandAvailable("node", ["--version"]),
|
|
72
|
+
commandAvailable("npm", ["--version"]),
|
|
73
|
+
commandAvailable("python3", ["--version"]),
|
|
74
|
+
commandAvailable("conda", ["--version"]),
|
|
75
|
+
commandAvailable("R", ["--version"]),
|
|
76
|
+
commandAvailable("pdflatex", ["--version"]),
|
|
77
|
+
commandAvailable("latexmk", ["--version"]),
|
|
78
|
+
getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
|
|
79
|
+
listProjectSessions(projectRoot, 12),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const npmPrefixPolicy = evaluateCommandPolicy("npm --prefix round9-node-app test", config);
|
|
83
|
+
const cdNpmTestPolicy = evaluateCommandPolicy("cd round9-node-app && npm test", config);
|
|
84
|
+
const mkdirPolicy = evaluateCommandPolicy("mkdir -p round9-node-app", config);
|
|
85
|
+
const nodeTestPolicy = evaluateCommandPolicy("node --test round9-node-app/test/app.test.js", config);
|
|
86
|
+
const bashSyntaxPolicy = evaluateCommandPolicy("bash -n maintenance/setup-conda.sh", config);
|
|
87
|
+
const texPolicy = evaluateCommandPolicy("pdflatex -interaction=nonstopmode -halt-on-error docs/note.tex", config);
|
|
88
|
+
|
|
89
|
+
const checks = [
|
|
90
|
+
capability("node", node.available, node),
|
|
91
|
+
capability("npm", npm.available, npm),
|
|
92
|
+
capability("python3", python.available, python),
|
|
93
|
+
capability("conda", conda.available, conda.available ? conda : { ...conda, setup: "Optional. Generate a dry-run Miniforge setup plan under maintenance/ before installing." }),
|
|
94
|
+
capability("R", r.available, r.available ? r : { ...r, setup: "Optional. Generate a project-local R setup plan; do not install globally from the agent." }),
|
|
95
|
+
capability("pdflatex", pdflatex.available, pdflatex.available ? pdflatex : { ...pdflatex, setup: "LaTeX tasks should create .tex source and an honest setup report when TeX is unavailable." }),
|
|
96
|
+
capability("latexmk", latexmk.available, latexmk.available ? latexmk : { ...latexmk, setup: "latexmk is optional if pdflatex is available." }),
|
|
97
|
+
capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
|
|
98
|
+
capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
|
|
99
|
+
capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
|
|
100
|
+
capability("file-tools", Boolean(config.allowFileTools), { workspace: config.commandCwd }),
|
|
101
|
+
capability("shell-tool", Boolean(config.allowShellTool), {
|
|
102
|
+
sandboxMode: config.sandboxMode,
|
|
103
|
+
packageInstallPolicy: config.packageInstallPolicy,
|
|
104
|
+
}),
|
|
105
|
+
capability("npm-prefix-test-policy", Boolean(npmPrefixPolicy.allowed), npmPrefixPolicy),
|
|
106
|
+
capability("cd-npm-test-policy", Boolean(cdNpmTestPolicy.allowed), cdNpmTestPolicy),
|
|
107
|
+
capability("mkdir-policy", Boolean(mkdirPolicy.allowed), mkdirPolicy),
|
|
108
|
+
capability("node-test-policy", Boolean(nodeTestPolicy.allowed), nodeTestPolicy),
|
|
109
|
+
capability("bash-syntax-policy", Boolean(bashSyntaxPolicy.allowed), bashSyntaxPolicy),
|
|
110
|
+
capability("tex-policy", Boolean(texPolicy.allowed), texPolicy),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
generatedAt: new Date().toISOString(),
|
|
116
|
+
package: {
|
|
117
|
+
name: "@lazyingart/agintiflow",
|
|
118
|
+
version: packageVersion,
|
|
119
|
+
},
|
|
120
|
+
project: {
|
|
121
|
+
root: paths.root,
|
|
122
|
+
commandCwd: path.resolve(config.commandCwd),
|
|
123
|
+
sessionsDir: paths.sessionsDir,
|
|
124
|
+
sessionDbPath: paths.sessionDbPath,
|
|
125
|
+
sharedSessionFolder: path.resolve(config.sessionsDir) === path.resolve(paths.sessionsDir),
|
|
126
|
+
},
|
|
127
|
+
routing: {
|
|
128
|
+
active: {
|
|
129
|
+
provider: config.provider,
|
|
130
|
+
model: config.model,
|
|
131
|
+
routingMode: config.routingMode,
|
|
132
|
+
routeReason: config.routeReason,
|
|
133
|
+
},
|
|
134
|
+
presets: getModelPresets(),
|
|
135
|
+
},
|
|
136
|
+
keys: {
|
|
137
|
+
deepseek: keyStatus.deepseek,
|
|
138
|
+
openai: keyStatus.openai,
|
|
139
|
+
mock: true,
|
|
140
|
+
localEnv: keyStatus.localEnv,
|
|
141
|
+
envVars: keyStatus.envVars,
|
|
142
|
+
},
|
|
143
|
+
tools: {
|
|
144
|
+
wrappers: listAgentWrappers().map((wrapper) => ({
|
|
145
|
+
name: wrapper.name,
|
|
146
|
+
label: wrapper.label,
|
|
147
|
+
available: wrapper.available,
|
|
148
|
+
role: wrapper.role,
|
|
149
|
+
})),
|
|
150
|
+
taskProfiles: listTaskProfiles().map((profile) => ({
|
|
151
|
+
id: profile.id,
|
|
152
|
+
label: profile.label,
|
|
153
|
+
tools: profile.tools,
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
checks,
|
|
157
|
+
maintenancePolicy: maintenancePolicyChecks(config),
|
|
158
|
+
sessions,
|
|
159
|
+
actionableSetup: checks
|
|
160
|
+
.filter((check) => !check.ok && check.setup)
|
|
161
|
+
.map((check) => ({
|
|
162
|
+
capability: check.name,
|
|
163
|
+
setup: check.setup,
|
|
164
|
+
})),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function printCapabilityReport(report) {
|
|
169
|
+
console.log(`AgInTiFlow capabilities ${report.package.version}`);
|
|
170
|
+
console.log(`project=${report.project.root}`);
|
|
171
|
+
console.log(`cwd=${report.project.commandCwd}`);
|
|
172
|
+
console.log(`sessions=${report.project.sessionsDir}`);
|
|
173
|
+
console.log(`sessionDb=${report.project.sessionDbPath}`);
|
|
174
|
+
console.log(`sharedSessions=${report.project.sharedSessionFolder}`);
|
|
175
|
+
console.log(
|
|
176
|
+
`route=${report.routing.active.routingMode} ${report.routing.active.provider}/${report.routing.active.model}`
|
|
177
|
+
);
|
|
178
|
+
console.log(
|
|
179
|
+
`keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
|
|
180
|
+
report.keys.openai ? "available" : "missing"
|
|
181
|
+
} mock=available localEnv=${report.keys.localEnv}`
|
|
182
|
+
);
|
|
183
|
+
for (const check of report.checks) {
|
|
184
|
+
const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
|
|
185
|
+
console.log(`${check.ok ? "OK" : "MISS"} ${check.name}${suffix}`);
|
|
186
|
+
}
|
|
187
|
+
if (report.actionableSetup.length > 0) {
|
|
188
|
+
console.log("setup:");
|
|
189
|
+
for (const item of report.actionableSetup) {
|
|
190
|
+
console.log(`- ${item.capability}: ${item.setup}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ 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 { buildCapabilityReport, printCapabilityReport } from "./capabilities.js";
|
|
6
7
|
import {
|
|
7
8
|
doctorReport,
|
|
8
9
|
initProject,
|
|
@@ -317,13 +318,42 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
if (argv[0] === "doctor") {
|
|
320
|
-
const
|
|
321
|
-
const
|
|
321
|
+
const parsed = parseArgs(argv.slice(1).filter((arg) => arg !== "--json" && arg !== "--capabilities"));
|
|
322
|
+
const config = loadConfig(
|
|
323
|
+
{
|
|
324
|
+
...parsed,
|
|
325
|
+
goal: "doctor",
|
|
326
|
+
allowShellTool: parsed.allowShellTool ?? true,
|
|
327
|
+
allowFileTools: parsed.allowFileTools ?? true,
|
|
328
|
+
},
|
|
329
|
+
{ packageDir, baseDir: process.cwd() }
|
|
330
|
+
);
|
|
331
|
+
const report = argv.includes("--capabilities")
|
|
332
|
+
? await buildCapabilityReport(process.cwd(), packageJson.version, config)
|
|
333
|
+
: await doctorReport(process.cwd(), packageJson.version, config);
|
|
322
334
|
if (argv.includes("--json")) console.log(JSON.stringify(report, null, 2));
|
|
335
|
+
else if (argv.includes("--capabilities")) printCapabilityReport(report);
|
|
323
336
|
else printDoctorReport(report);
|
|
324
337
|
return;
|
|
325
338
|
}
|
|
326
339
|
|
|
340
|
+
if (argv[0] === "capabilities") {
|
|
341
|
+
const parsed = parseArgs(argv.slice(1).filter((arg) => arg !== "--json"));
|
|
342
|
+
const config = loadConfig(
|
|
343
|
+
{
|
|
344
|
+
...parsed,
|
|
345
|
+
goal: "capabilities",
|
|
346
|
+
allowShellTool: parsed.allowShellTool ?? true,
|
|
347
|
+
allowFileTools: parsed.allowFileTools ?? true,
|
|
348
|
+
},
|
|
349
|
+
{ packageDir, baseDir: process.cwd() }
|
|
350
|
+
);
|
|
351
|
+
const report = await buildCapabilityReport(process.cwd(), packageJson.version, config);
|
|
352
|
+
if (argv.includes("--json")) console.log(JSON.stringify(report, null, 2));
|
|
353
|
+
else printCapabilityReport(report);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
if (argv[0] === "keys/status") {
|
|
328
358
|
await handleKeyCommand(["status"]);
|
|
329
359
|
return;
|
package/src/command-policy.js
CHANGED
|
@@ -27,12 +27,19 @@ const READ_ONLY_PATTERNS = [
|
|
|
27
27
|
|
|
28
28
|
const TEST_PATTERNS = [
|
|
29
29
|
/^npm\s+(run\s+)?(check|test|build|lint)(?:\s+--\s+[-\w./:=]+)*$/,
|
|
30
|
+
/^npm\s+--prefix\s+[-\w./]+\s+(run\s+)?(check|test|build|lint)(?:\s+--\s+[-\w./:=]+)*$/,
|
|
30
31
|
/^npm\s+test$/,
|
|
31
32
|
/^node\s+--check\s+[-\w./]+$/,
|
|
33
|
+
/^node\s+--test(?:\s+[-\w./]+)*$/,
|
|
34
|
+
/^bash\s+-n\s+[-\w./]+\.sh$/,
|
|
35
|
+
/^sh\s+-n\s+[-\w./]+\.sh$/,
|
|
36
|
+
/^python(?:3)?\s+-m\s+py_compile\s+[-\w./]+\.py$/,
|
|
32
37
|
/^python(?:3)?\s+-m\s+pytest(?:\s+[-\w./:=]+)*$/,
|
|
33
38
|
/^pytest(?:\s+[-\w./:=]+)*$/,
|
|
34
39
|
];
|
|
35
40
|
|
|
41
|
+
const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
|
|
42
|
+
|
|
36
43
|
const TOOLCHAIN_PATTERNS = [
|
|
37
44
|
/^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
|
|
38
45
|
/^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
|
|
@@ -107,10 +114,19 @@ function matchAny(patterns, command) {
|
|
|
107
114
|
return patterns.some((pattern) => pattern.test(command));
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
const normalized = String(
|
|
112
|
-
if (!normalized
|
|
117
|
+
function isSafeRelativeDir(value) {
|
|
118
|
+
const normalized = String(value || "").trim();
|
|
119
|
+
if (!normalized || normalized.startsWith("/") || normalized.startsWith("~")) return false;
|
|
120
|
+
return normalized.split("/").every((part) => part && part !== "." && part !== "..");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSafeVirtualWorkspaceDir(value) {
|
|
124
|
+
const normalized = String(value || "").trim();
|
|
125
|
+
if (!normalized.startsWith("/workspace/")) return false;
|
|
126
|
+
return isSafeRelativeDir(normalized.replace(/^\/workspace\//, ""));
|
|
127
|
+
}
|
|
113
128
|
|
|
129
|
+
function classifySimpleCommand(normalized) {
|
|
114
130
|
if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
115
131
|
return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
|
|
116
132
|
}
|
|
@@ -119,6 +135,14 @@ export function classifyCommand(command) {
|
|
|
119
135
|
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
120
136
|
return { category: "blocked", reason: `Command contains blocked shell syntax: ${normalized}` };
|
|
121
137
|
}
|
|
138
|
+
if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
|
|
139
|
+
const target = normalized.replace(/^mkdir\s+-p\s+/, "");
|
|
140
|
+
const virtualWorkspacePath = isSafeVirtualWorkspaceDir(target);
|
|
141
|
+
if (!isSafeRelativeDir(target) && !virtualWorkspacePath) {
|
|
142
|
+
return { category: "blocked", reason: `mkdir target must be a safe workspace-relative directory: ${target}` };
|
|
143
|
+
}
|
|
144
|
+
return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
|
|
145
|
+
}
|
|
122
146
|
if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
|
|
123
147
|
return { category: "blocked", reason: `Command contains a write-capable or network token: ${normalized}` };
|
|
124
148
|
}
|
|
@@ -142,6 +166,26 @@ export function classifyCommand(command) {
|
|
|
142
166
|
return { category: "blocked", reason: `Command is outside the execution allowlist: ${normalized}` };
|
|
143
167
|
}
|
|
144
168
|
|
|
169
|
+
function classifyCdCommand(normalized) {
|
|
170
|
+
const match = normalized.match(/^cd\s+([-\w./]+)\s+&&\s+(.+)$/);
|
|
171
|
+
if (!match) return null;
|
|
172
|
+
const [, dir, inner] = match;
|
|
173
|
+
const virtualWorkspacePath = isSafeVirtualWorkspaceDir(dir);
|
|
174
|
+
if (!isSafeRelativeDir(dir) && !virtualWorkspacePath) {
|
|
175
|
+
return { category: "blocked", reason: `cd target must be a safe workspace-relative directory: ${dir}` };
|
|
176
|
+
}
|
|
177
|
+
const innerClassification = classifySimpleCommand(inner.trim());
|
|
178
|
+
if (innerClassification.category === "blocked") return innerClassification;
|
|
179
|
+
return { ...innerClassification, cdDir: dir, virtualWorkspacePath };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function classifyCommand(command) {
|
|
183
|
+
const normalized = String(command || "").trim();
|
|
184
|
+
if (!normalized) return { category: "blocked", reason: "Command is empty." };
|
|
185
|
+
|
|
186
|
+
return classifyCdCommand(normalized) || classifySimpleCommand(normalized);
|
|
187
|
+
}
|
|
188
|
+
|
|
145
189
|
export function evaluateCommandPolicy(command, config) {
|
|
146
190
|
const classification = classifyCommand(command);
|
|
147
191
|
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
@@ -151,6 +195,16 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
151
195
|
return { allowed: false, ...classification, sandboxMode, packageInstallPolicy };
|
|
152
196
|
}
|
|
153
197
|
|
|
198
|
+
if (classification.virtualWorkspacePath && !config.useDockerSandbox) {
|
|
199
|
+
return {
|
|
200
|
+
allowed: false,
|
|
201
|
+
...classification,
|
|
202
|
+
reason: "Virtual /workspace shell paths are allowed only inside Docker sandbox mode.",
|
|
203
|
+
sandboxMode,
|
|
204
|
+
packageInstallPolicy,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
154
208
|
if (!config.allowShellTool) {
|
|
155
209
|
return {
|
|
156
210
|
allowed: false,
|
package/src/model-client.js
CHANGED
|
@@ -156,8 +156,10 @@ export async function createPlan(client, config, state) {
|
|
|
156
156
|
: "",
|
|
157
157
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
158
158
|
"A canvas/artifacts tunnel is available through send_to_canvas. Use it when an output should be highlighted visually, such as screenshots, image files, important markdown, diffs, or generated artifact paths. It is optional for ordinary text answers.",
|
|
159
|
-
"
|
|
160
|
-
"
|
|
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
|
+
"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, prefer project-local dry-run plans/scripts unless the configured policy explicitly allows stronger actions.",
|
|
162
|
+
"Plan for a complete result, not endless exploration; finish once the request is satisfied and checks have passed or been honestly skipped.",
|
|
161
163
|
"Return a numbered plan only.",
|
|
162
164
|
]
|
|
163
165
|
.filter(Boolean)
|
package/src/task-profiles.js
CHANGED
|
@@ -10,7 +10,7 @@ export const TASK_PROFILES = {
|
|
|
10
10
|
id: "code",
|
|
11
11
|
label: "Code writing",
|
|
12
12
|
prompt:
|
|
13
|
-
"Act like a coding agent:
|
|
13
|
+
"Act like a coding agent: understand the request, edit workspace files, run useful safe checks, and report changed files and residual risks.",
|
|
14
14
|
tools: ["files", "shell", "sandbox"],
|
|
15
15
|
},
|
|
16
16
|
writing: {
|
|
@@ -45,14 +45,21 @@ export const TASK_PROFILES = {
|
|
|
45
45
|
id: "node",
|
|
46
46
|
label: "Node",
|
|
47
47
|
prompt:
|
|
48
|
-
"For Node.js tasks,
|
|
48
|
+
"For Node.js tasks, use the local project structure, add tests when useful, and run safe npm/node checks when available.",
|
|
49
49
|
tools: ["files", "shell", "sandbox"],
|
|
50
50
|
},
|
|
51
|
+
website: {
|
|
52
|
+
id: "website",
|
|
53
|
+
label: "Website testing",
|
|
54
|
+
prompt:
|
|
55
|
+
"For website-testing tasks, create or inspect the site, add a simple local test/check file when useful, and run safe checks without installing packages unless approved.",
|
|
56
|
+
tools: ["files", "shell", "canvas", "sandbox"],
|
|
57
|
+
},
|
|
51
58
|
aaps: {
|
|
52
59
|
id: "aaps",
|
|
53
60
|
label: "AAPS",
|
|
54
61
|
prompt:
|
|
55
|
-
"For AAPS tasks, recognize .aaps folders and @lazyingart/aaps
|
|
62
|
+
"For AAPS tasks, recognize .aaps folders and @lazyingart/aaps workflows, keep work project-local, and avoid secrets or publishing.",
|
|
56
63
|
tools: ["files", "shell", "sandbox"],
|
|
57
64
|
},
|
|
58
65
|
latex: {
|
|
@@ -66,7 +73,7 @@ export const TASK_PROFILES = {
|
|
|
66
73
|
id: "maintenance",
|
|
67
74
|
label: "System maintenance",
|
|
68
75
|
prompt:
|
|
69
|
-
"For system maintenance, diagnose first, prefer
|
|
76
|
+
"For system maintenance, diagnose first, prefer reversible project-local plans or dry-run scripts, and avoid privileged/global/destructive actions unless explicitly approved.",
|
|
70
77
|
tools: ["shell", "sandbox", "files"],
|
|
71
78
|
},
|
|
72
79
|
};
|
package/web.js
CHANGED
|
@@ -13,6 +13,7 @@ import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./src/comma
|
|
|
13
13
|
import { summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./src/workspace-tools.js";
|
|
14
14
|
import { listTaskProfiles, normalizeTaskProfile } from "./src/task-profiles.js";
|
|
15
15
|
import { loadProjectEnv, projectPaths, providerKeyStatus, setProviderKey } from "./src/project.js";
|
|
16
|
+
import { buildCapabilityReport } from "./src/capabilities.js";
|
|
16
17
|
import {
|
|
17
18
|
buildArtifacts,
|
|
18
19
|
countUnreadArtifacts,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
26
|
const __dirname = path.dirname(__filename);
|
|
26
27
|
const packageDir = __dirname;
|
|
28
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
|
|
27
29
|
const baseDir = path.resolve(process.env.AGINTIFLOW_RUNTIME_DIR || process.cwd());
|
|
28
30
|
const sessionsDir = path.join(baseDir, ".sessions");
|
|
29
31
|
loadProjectEnv(baseDir);
|
|
@@ -539,6 +541,18 @@ app.get("/api/keys/status", (_req, res) => {
|
|
|
539
541
|
res.json({ ok: true, keyStatus: publicKeyStatus(baseDir) });
|
|
540
542
|
});
|
|
541
543
|
|
|
544
|
+
app.get("/api/capabilities", async (_req, res) => {
|
|
545
|
+
const preferences = normalizePreferencePayload({}, db.getPreferences());
|
|
546
|
+
const config = buildRunConfig({
|
|
547
|
+
...preferences,
|
|
548
|
+
goal: "capabilities",
|
|
549
|
+
allowShellTool: true,
|
|
550
|
+
allowFileTools: true,
|
|
551
|
+
});
|
|
552
|
+
const report = await buildCapabilityReport(baseDir, packageJson.version, config);
|
|
553
|
+
res.json(report);
|
|
554
|
+
});
|
|
555
|
+
|
|
542
556
|
app.post("/api/keys/:provider", async (req, res) => {
|
|
543
557
|
try {
|
|
544
558
|
const result = await setProviderKey(baseDir, req.params.provider, req.body?.apiKey || req.body?.key || "");
|