@lazyingart/agintiflow 0.4.1 → 0.6.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 +32 -6
- 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 +82 -0
- package/scripts/smoke-web-api.js +11 -0
- package/src/agent-runner.js +33 -20
- package/src/capabilities.js +231 -0
- package/src/cli.js +38 -3
- package/src/command-policy.js +157 -15
- package/src/config.js +1 -1
- package/src/docker-sandbox.js +1 -1
- package/src/model-client.js +5 -3
- package/src/task-profiles.js +11 -4
- package/web.js +14 -0
package/README.md
CHANGED
|
@@ -66,10 +66,13 @@ 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"
|
|
72
74
|
aginti --profile code --provider mock --routing manual "Create notes/hello.md"
|
|
75
|
+
aginti --allow-shell --sandbox-mode docker-workspace --approve-package-installs "set up and test this project"
|
|
73
76
|
```
|
|
74
77
|
|
|
75
78
|
Run from a source checkout:
|
|
@@ -113,6 +116,29 @@ npm start -- --resume your-session-id
|
|
|
113
116
|
|
|
114
117
|
The package exposes both `aginti` and `aginti-cli`; they run the same CLI entrypoint.
|
|
115
118
|
|
|
119
|
+
## Capability Checks
|
|
120
|
+
|
|
121
|
+
Use the capability report to verify a project folder before running real agent work:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
aginti capabilities
|
|
125
|
+
aginti capabilities --json
|
|
126
|
+
aginti doctor --capabilities
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
Live DeepSeek verification is opt-in because it spends provider credits:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
AGINTIFLOW_REAL_DEEPSEEK=1 \
|
|
135
|
+
AGINTIFLOW_REAL_WORKSPACE=/home/lachlan/ProjectsLFS/aginti-test \
|
|
136
|
+
AGINTIFLOW_REAL_WEB_BASE_URL=http://127.0.0.1:3220 \
|
|
137
|
+
npm run real:deepseek
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
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).
|
|
141
|
+
|
|
116
142
|
## Web UI
|
|
117
143
|
|
|
118
144
|
The web app includes:
|
|
@@ -156,12 +182,12 @@ AgInTiFlow is intentionally conservative:
|
|
|
156
182
|
- Shell commands are disabled unless the shell tool is enabled.
|
|
157
183
|
- Workspace file tools stay inside `commandCwd` and block `.env`, secret-like paths, `.git`, node_modules writes, absolute escapes, binary files, and huge files.
|
|
158
184
|
- File writes record before/after SHA-256 hashes and compact redacted diffs.
|
|
159
|
-
- Guarded shell mode
|
|
185
|
+
- 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.
|
|
160
186
|
- Docker read-only mode mounts the workspace read-only and disables container network access.
|
|
161
187
|
- Docker workspace-write mode is the web UI default so plot, PDF, and test outputs can be written inside the mounted workspace.
|
|
162
|
-
- Docker workspace-write mode is
|
|
188
|
+
- 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.
|
|
163
189
|
- Package installs default to `prompt`, so npm/pip/conda/venv setup is blocked until explicitly approved.
|
|
164
|
-
- NPM publishing, npm token commands,
|
|
190
|
+
- 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.
|
|
165
191
|
- NPM tokens and API keys are redacted from tool logs and API responses.
|
|
166
192
|
- Every tool request and result is written to structured logs.
|
|
167
193
|
|
|
@@ -286,9 +312,9 @@ The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sa
|
|
|
286
312
|
|
|
287
313
|
| Mode | Workspace mount | Network | Intended use |
|
|
288
314
|
| --- | --- | --- | --- |
|
|
289
|
-
| `host` | local process | host network |
|
|
315
|
+
| `host` | local process | host network | direct project work; privileged/destructive commands require explicit trust |
|
|
290
316
|
| `docker-readonly` | read-only `/workspace` | none | safe coding inspection and tests that do not write files |
|
|
291
|
-
| `docker-workspace` | writable `/workspace` | none by default, enabled
|
|
317
|
+
| `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 |
|
|
292
318
|
|
|
293
319
|
Package policy values:
|
|
294
320
|
|
|
@@ -296,7 +322,7 @@ Package policy values:
|
|
|
296
322
|
| --- | --- |
|
|
297
323
|
| `block` | Always block npm/pip/conda/venv setup. |
|
|
298
324
|
| `prompt` | Return a clear approval-required error; the UI can switch to approved. |
|
|
299
|
-
| `allow` | Permit
|
|
325
|
+
| `allow` | Permit package/setup commands. Docker workspace mode also allows broader shell/network commands while keeping secrets and npm publishing blocked. |
|
|
300
326
|
|
|
301
327
|
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 they require `docker-workspace` because they write outputs back to `/workspace`. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
|
|
302
328
|
|
|
@@ -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 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
|
+
- `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.6.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 useful checks under the configured policy.`,
|
|
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 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
|
+
{
|
|
338
|
+
routingMode: "complex",
|
|
339
|
+
taskProfile: "maintenance",
|
|
340
|
+
sandboxMode: "docker-workspace",
|
|
341
|
+
packageInstallPolicy: "allow",
|
|
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,82 @@
|
|
|
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
|
+
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
|
+
);
|
|
65
|
+
|
|
66
|
+
const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
|
|
67
|
+
assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
|
|
68
|
+
|
|
69
|
+
console.log(
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
{
|
|
72
|
+
ok: true,
|
|
73
|
+
projectRoot: tempRoot,
|
|
74
|
+
checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
82
|
+
}
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -83,6 +83,16 @@ 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
|
+
}
|
|
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
|
+
}
|
|
86
96
|
const savedKey = await fetchJson("/api/keys/deepseek", {
|
|
87
97
|
method: "POST",
|
|
88
98
|
headers: { "Content-Type": "application/json" },
|
|
@@ -231,6 +241,7 @@ try {
|
|
|
231
241
|
endpoints: [
|
|
232
242
|
"/api/config",
|
|
233
243
|
"/api/keys/status",
|
|
244
|
+
"/api/capabilities",
|
|
234
245
|
"POST /api/keys/:provider",
|
|
235
246
|
"/api/sandbox/status",
|
|
236
247
|
"/api/sandbox/preflight",
|