@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 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 only allows inspection, test/build checks, and approved setup commands.
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 required for approved package or environment setup.
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, sudo, destructive git actions, curl/wget, and shell chaining are blocked.
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 | legacy read-only inspection only |
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 only for approved package installs | web UI default for environment setup, plotting, and LaTeX/PDF compilation inside the mounted project |
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 allowlisted setup commands only in `docker-workspace`. |
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.4.1",
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
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools",
63
- "pack:dry-run": "npm pack --dry-run"
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
+ }
@@ -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",