@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.
@@ -144,8 +144,8 @@ function createInitialState(config, sessionId) {
144
144
  "Avoid destructive actions, purchases, account changes, and sensitive workflows.",
145
145
  config.allowShellTool
146
146
  ? config.useDockerSandbox
147
- ? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Read-only commands have no network. Package installs or environment setup require approved package policy and Docker workspace-write mode.`
148
- : "A read-only host shell command tool is available for short local inspection tasks."
147
+ ? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Docker workspace mode with approved package installs supports broader setup and network commands.`
148
+ : "A host shell command tool is available under the configured trust policy."
149
149
  : "No shell command tool is available.",
150
150
  config.allowFileTools
151
151
  ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, and apply_patch. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked.`
@@ -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
- "For LaTeX/PDF requests, create the needed source/assets, compile with the available allowlisted TeX toolchain, and publish the resulting PDF through send_to_canvas. For subfolder documents, keep outputs beside the source. Use pdflatex-compatible figure formats such as PDF or PNG.",
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, use the configured sandbox and package policy; Docker workspace mode is the preferred place for installs and toolchain setup.",
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
- "LaTeX/PDF requests should produce source artifacts and, when possible, a compiled PDF artifact. For subfolder documents, keep outputs beside the source. For figure-in-document tasks, create PDF/PNG figures that pdflatex can include.",
184
+ "Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
185
+ "Use the configured sandbox and package policy for environment or system-maintenance work.",
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
- if (config.useDockerSandbox) {
373
- return runDockerSandboxCommand(command, config, policy);
374
- }
375
-
376
- const result = await exec(command, {
377
- cwd: config.commandCwd,
378
- timeout: 5000,
379
- maxBuffer: 200 * 1024,
380
- shell: "/bin/bash",
381
- env: safeExecutionEnv(),
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
- return {
385
- stdout: redactSensitiveText(result.stdout).trim().slice(0, 8000),
386
- stderr: redactSensitiveText(result.stderr).trim().slice(0, 4000),
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: true,
581
+ ok: commandResult.ok !== false,
569
582
  toolName: "run_command",
570
583
  args: safeArgs,
571
584
  sandbox: config.useDockerSandbox ? "docker" : "host",
@@ -0,0 +1,231 @@
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
+ function trustedDockerPolicyChecks(config) {
68
+ const dockerConfig = {
69
+ ...config,
70
+ allowShellTool: true,
71
+ allowDestructive: false,
72
+ sandboxMode: "docker-workspace",
73
+ useDockerSandbox: true,
74
+ packageInstallPolicy: "allow",
75
+ };
76
+ const sampleCommands = [
77
+ "apt-get update",
78
+ "apt-get install -y curl wget",
79
+ "wget https://example.com/file.txt",
80
+ "curl -fsSL https://example.com/file.txt -o downloads/file.txt",
81
+ "npm install lodash",
82
+ "python3 -m pip install requests",
83
+ ];
84
+
85
+ return sampleCommands.map((command) => {
86
+ const policy = evaluateCommandPolicy(command, dockerConfig);
87
+ return {
88
+ command,
89
+ allowed: Boolean(policy.allowed),
90
+ category: policy.category || classifyCommand(command).category,
91
+ reason: policy.reason || "",
92
+ needsNetwork: Boolean(policy.needsNetwork),
93
+ requiresDockerRoot: Boolean(policy.requiresDockerRoot),
94
+ sandboxMode: policy.sandboxMode,
95
+ packageInstallPolicy: policy.packageInstallPolicy,
96
+ };
97
+ });
98
+ }
99
+
100
+ export async function buildCapabilityReport(projectRoot, packageVersion, config) {
101
+ const paths = projectPaths(projectRoot);
102
+ const keyStatus = providerKeyStatus(projectRoot);
103
+ const [node, npm, python, conda, r, pdflatex, latexmk, dockerStatus, sessions] = await Promise.all([
104
+ commandAvailable("node", ["--version"]),
105
+ commandAvailable("npm", ["--version"]),
106
+ commandAvailable("python3", ["--version"]),
107
+ commandAvailable("conda", ["--version"]),
108
+ commandAvailable("R", ["--version"]),
109
+ commandAvailable("pdflatex", ["--version"]),
110
+ commandAvailable("latexmk", ["--version"]),
111
+ getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
112
+ listProjectSessions(projectRoot, 12),
113
+ ]);
114
+
115
+ const npmPrefixPolicy = evaluateCommandPolicy("npm --prefix round9-node-app test", config);
116
+ const cdNpmTestPolicy = evaluateCommandPolicy("cd round9-node-app && npm test", config);
117
+ const mkdirPolicy = evaluateCommandPolicy("mkdir -p round9-node-app", config);
118
+ const nodeTestPolicy = evaluateCommandPolicy("node --test round9-node-app/test/app.test.js", config);
119
+ const bashSyntaxPolicy = evaluateCommandPolicy("bash -n maintenance/setup-conda.sh", config);
120
+ const texPolicy = evaluateCommandPolicy("pdflatex -interaction=nonstopmode -halt-on-error docs/note.tex", config);
121
+
122
+ const checks = [
123
+ capability("node", node.available, node),
124
+ capability("npm", npm.available, npm),
125
+ capability("python3", python.available, python),
126
+ capability("conda", conda.available, conda.available ? conda : { ...conda, setup: "Optional. Generate a dry-run Miniforge setup plan under maintenance/ before installing." }),
127
+ capability("R", r.available, r.available ? r : { ...r, setup: "Optional. Generate a project-local R setup plan; do not install globally from the agent." }),
128
+ capability("pdflatex", pdflatex.available, pdflatex.available ? pdflatex : { ...pdflatex, setup: "LaTeX tasks should create .tex source and an honest setup report when TeX is unavailable." }),
129
+ capability("latexmk", latexmk.available, latexmk.available ? latexmk : { ...latexmk, setup: "latexmk is optional if pdflatex is available." }),
130
+ capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
131
+ capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
132
+ capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
133
+ capability("file-tools", Boolean(config.allowFileTools), { workspace: config.commandCwd }),
134
+ capability("shell-tool", Boolean(config.allowShellTool), {
135
+ sandboxMode: config.sandboxMode,
136
+ packageInstallPolicy: config.packageInstallPolicy,
137
+ }),
138
+ capability("npm-prefix-test-policy", Boolean(npmPrefixPolicy.allowed), npmPrefixPolicy),
139
+ capability("cd-npm-test-policy", Boolean(cdNpmTestPolicy.allowed), cdNpmTestPolicy),
140
+ capability("mkdir-policy", Boolean(mkdirPolicy.allowed), mkdirPolicy),
141
+ capability("node-test-policy", Boolean(nodeTestPolicy.allowed), nodeTestPolicy),
142
+ capability("bash-syntax-policy", Boolean(bashSyntaxPolicy.allowed), bashSyntaxPolicy),
143
+ capability("tex-policy", Boolean(texPolicy.allowed), texPolicy),
144
+ ];
145
+
146
+ return {
147
+ ok: true,
148
+ generatedAt: new Date().toISOString(),
149
+ package: {
150
+ name: "@lazyingart/agintiflow",
151
+ version: packageVersion,
152
+ },
153
+ project: {
154
+ root: paths.root,
155
+ commandCwd: path.resolve(config.commandCwd),
156
+ sessionsDir: paths.sessionsDir,
157
+ sessionDbPath: paths.sessionDbPath,
158
+ sharedSessionFolder: path.resolve(config.sessionsDir) === path.resolve(paths.sessionsDir),
159
+ },
160
+ routing: {
161
+ active: {
162
+ provider: config.provider,
163
+ model: config.model,
164
+ routingMode: config.routingMode,
165
+ routeReason: config.routeReason,
166
+ },
167
+ presets: getModelPresets(),
168
+ },
169
+ keys: {
170
+ deepseek: keyStatus.deepseek,
171
+ openai: keyStatus.openai,
172
+ mock: true,
173
+ localEnv: keyStatus.localEnv,
174
+ envVars: keyStatus.envVars,
175
+ },
176
+ tools: {
177
+ wrappers: listAgentWrappers().map((wrapper) => ({
178
+ name: wrapper.name,
179
+ label: wrapper.label,
180
+ available: wrapper.available,
181
+ role: wrapper.role,
182
+ })),
183
+ taskProfiles: listTaskProfiles().map((profile) => ({
184
+ id: profile.id,
185
+ label: profile.label,
186
+ tools: profile.tools,
187
+ })),
188
+ },
189
+ checks,
190
+ maintenancePolicy: maintenancePolicyChecks(config),
191
+ trustedDockerPolicy: trustedDockerPolicyChecks(config),
192
+ sessions,
193
+ actionableSetup: checks
194
+ .filter((check) => !check.ok && check.setup)
195
+ .map((check) => ({
196
+ capability: check.name,
197
+ setup: check.setup,
198
+ })),
199
+ };
200
+ }
201
+
202
+ export function printCapabilityReport(report) {
203
+ console.log(`AgInTiFlow capabilities ${report.package.version}`);
204
+ console.log(`project=${report.project.root}`);
205
+ console.log(`cwd=${report.project.commandCwd}`);
206
+ console.log(`sessions=${report.project.sessionsDir}`);
207
+ console.log(`sessionDb=${report.project.sessionDbPath}`);
208
+ console.log(`sharedSessions=${report.project.sharedSessionFolder}`);
209
+ console.log(
210
+ `route=${report.routing.active.routingMode} ${report.routing.active.provider}/${report.routing.active.model}`
211
+ );
212
+ console.log(
213
+ `keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
214
+ report.keys.openai ? "available" : "missing"
215
+ } mock=available localEnv=${report.keys.localEnv}`
216
+ );
217
+ for (const check of report.checks) {
218
+ const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
219
+ console.log(`${check.ok ? "OK" : "MISS"} ${check.name}${suffix}`);
220
+ }
221
+ if (report.trustedDockerPolicy?.length) {
222
+ const allowed = report.trustedDockerPolicy.filter((item) => item.allowed).length;
223
+ console.log(`trustedDockerPolicy=${allowed}/${report.trustedDockerPolicy.length} allowed when sandbox=docker-workspace packageInstalls=allow`);
224
+ }
225
+ if (report.actionableSetup.length > 0) {
226
+ console.log("setup:");
227
+ for (const item of report.actionableSetup) {
228
+ console.log(`- ${item.capability}: ${item.setup}`);
229
+ }
230
+ }
231
+ }
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,
@@ -40,6 +41,7 @@ export function parseArgs(argv) {
40
41
  allowShellTool: undefined,
41
42
  allowFileTools: undefined,
42
43
  allowWrapperTools: undefined,
44
+ allowDestructive: undefined,
43
45
  preferredWrapper: "",
44
46
  taskProfile: "",
45
47
  useDockerSandbox: undefined,
@@ -131,6 +133,10 @@ export function parseArgs(argv) {
131
133
  result.allowShellTool = true;
132
134
  continue;
133
135
  }
136
+ if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
137
+ result.allowDestructive = true;
138
+ continue;
139
+ }
134
140
  if (arg === "--allow-file-tools") {
135
141
  result.allowFileTools = true;
136
142
  continue;
@@ -317,13 +323,42 @@ export async function main(argv = process.argv.slice(2)) {
317
323
  }
318
324
 
319
325
  if (argv[0] === "doctor") {
320
- const config = loadConfig({ goal: "doctor" }, { packageDir, baseDir: process.cwd() });
321
- const report = await doctorReport(process.cwd(), packageJson.version, config);
326
+ const parsed = parseArgs(argv.slice(1).filter((arg) => arg !== "--json" && arg !== "--capabilities"));
327
+ const config = loadConfig(
328
+ {
329
+ ...parsed,
330
+ goal: "doctor",
331
+ allowShellTool: parsed.allowShellTool ?? true,
332
+ allowFileTools: parsed.allowFileTools ?? true,
333
+ },
334
+ { packageDir, baseDir: process.cwd() }
335
+ );
336
+ const report = argv.includes("--capabilities")
337
+ ? await buildCapabilityReport(process.cwd(), packageJson.version, config)
338
+ : await doctorReport(process.cwd(), packageJson.version, config);
322
339
  if (argv.includes("--json")) console.log(JSON.stringify(report, null, 2));
340
+ else if (argv.includes("--capabilities")) printCapabilityReport(report);
323
341
  else printDoctorReport(report);
324
342
  return;
325
343
  }
326
344
 
345
+ if (argv[0] === "capabilities") {
346
+ const parsed = parseArgs(argv.slice(1).filter((arg) => arg !== "--json"));
347
+ const config = loadConfig(
348
+ {
349
+ ...parsed,
350
+ goal: "capabilities",
351
+ allowShellTool: parsed.allowShellTool ?? true,
352
+ allowFileTools: parsed.allowFileTools ?? true,
353
+ },
354
+ { packageDir, baseDir: process.cwd() }
355
+ );
356
+ const report = await buildCapabilityReport(process.cwd(), packageJson.version, config);
357
+ if (argv.includes("--json")) console.log(JSON.stringify(report, null, 2));
358
+ else printCapabilityReport(report);
359
+ return;
360
+ }
361
+
327
362
  if (argv[0] === "keys/status") {
328
363
  await handleKeyCommand(["status"]);
329
364
  return;
@@ -399,7 +434,7 @@ export async function main(argv = process.argv.slice(2)) {
399
434
 
400
435
  if (!args.goal && !args.resume) {
401
436
  console.error(
402
- 'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--allow-shell] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
437
+ 'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
403
438
  );
404
439
  process.exit(1);
405
440
  }