@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.
@@ -27,12 +27,24 @@ 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
+
43
+ const NETWORK_FETCH_PATTERNS = [
44
+ /^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
45
+ /^wget\s+(?:-[A-Za-z0-9]*\s+)*(?:-O\s+[-\w./]+\s+)?https?:\/\/\S+$/,
46
+ ];
47
+
36
48
  const TOOLCHAIN_PATTERNS = [
37
49
  /^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
38
50
  /^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
@@ -41,12 +53,24 @@ const TOOLCHAIN_PATTERNS = [
41
53
 
42
54
  const PACKAGE_INSTALL_PATTERNS = [
43
55
  /^npm\s+ci$/,
44
- /^npm\s+install$/,
56
+ /^npm\s+install(?:\s+[-@\w./:=]+)*$/,
45
57
  /^pnpm\s+install$/,
58
+ /^pnpm\s+add(?:\s+[-@\w./:=]+)+$/,
46
59
  /^yarn\s+install$/,
60
+ /^yarn\s+add(?:\s+[-@\w./:=]+)+$/,
47
61
  /^python(?:3)?\s+-m\s+pip\s+install\s+-r\s+[-\w./]+$/,
62
+ /^python(?:3)?\s+-m\s+pip\s+install(?:\s+[-@\w./:=]+)+$/,
48
63
  /^pip(?:3)?\s+install\s+-r\s+[-\w./]+$/,
64
+ /^pip(?:3)?\s+install(?:\s+[-@\w./:=]+)+$/,
65
+ /^uv\s+(sync|pip\s+install)(?:\s+[-@\w./:=]+)*$/,
49
66
  /^conda\s+env\s+(create|update)\s+-f\s+[-\w./]+$/,
67
+ /^conda\s+install(?:\s+[-@\w./:=]+)+$/,
68
+ ];
69
+
70
+ const SYSTEM_PACKAGE_INSTALL_PATTERNS = [
71
+ /^(?:sudo\s+)?apt(?:-get)?\s+update$/,
72
+ /^(?:sudo\s+)?apt(?:-get)?\s+install(?:\s+-y)?(?:\s+[-@\w.+:=]+)+$/,
73
+ /^apk\s+add(?:\s+--no-cache)?(?:\s+[-@\w.+:=]+)+$/,
50
74
  ];
51
75
 
52
76
  const ENV_SETUP_PATTERNS = [
@@ -57,13 +81,10 @@ const ENV_SETUP_PATTERNS = [
57
81
 
58
82
  const BLOCKED_SHELL_TOKENS = ["&&", "||", ";", "|", ">", "<", "$(", "`"];
59
83
  const BLOCKED_WRITE_TOKENS = [
60
- "sudo ",
61
84
  " rm",
62
85
  " mv",
63
- " cp",
64
86
  " chmod",
65
87
  " chown",
66
- " mkdir",
67
88
  " rmdir",
68
89
  " touch",
69
90
  " tee",
@@ -76,8 +97,6 @@ const BLOCKED_WRITE_TOKENS = [
76
97
  "git switch",
77
98
  "git reset",
78
99
  "git clean",
79
- "curl ",
80
- "wget ",
81
100
  ];
82
101
 
83
102
  const ALWAYS_BLOCKED_PATTERNS = [
@@ -91,6 +110,13 @@ const ALWAYS_BLOCKED_PATTERNS = [
91
110
  /DEEPSEEK_API_KEY\s*=/i,
92
111
  ];
93
112
 
113
+ const SENSITIVE_COMMAND_PATTERNS = [
114
+ /(^|[\s./])\.env(\s|$|[./])/i,
115
+ /(^|[\s./])\.npmrc(\s|$|[./])/i,
116
+ /^(env|printenv)(\s|$)/i,
117
+ /(api[_-]?key|auth[_-]?token|npm[_-]?token|_authToken|bearer\s+[A-Za-z0-9._-]+)/i,
118
+ ];
119
+
94
120
  function normalizePolicy(value, allowed, fallback) {
95
121
  return allowed.includes(value) ? value : fallback;
96
122
  }
@@ -107,20 +133,50 @@ function matchAny(patterns, command) {
107
133
  return patterns.some((pattern) => pattern.test(command));
108
134
  }
109
135
 
110
- export function classifyCommand(command) {
111
- const normalized = String(command || "").trim();
112
- if (!normalized) return { category: "blocked", reason: "Command is empty." };
136
+ function isSafeRelativeDir(value) {
137
+ const normalized = String(value || "").trim();
138
+ if (!normalized || normalized.startsWith("/") || normalized.startsWith("~")) return false;
139
+ return normalized.split("/").every((part) => part && part !== "." && part !== "..");
140
+ }
141
+
142
+ function isSafeVirtualWorkspaceDir(value) {
143
+ const normalized = String(value || "").trim();
144
+ if (!normalized.startsWith("/workspace/")) return false;
145
+ return isSafeRelativeDir(normalized.replace(/^\/workspace\//, ""));
146
+ }
113
147
 
148
+ function classifySimpleCommand(normalized) {
114
149
  if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
115
150
  return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
116
151
  }
152
+ if (SENSITIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
153
+ return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
154
+ }
117
155
 
118
156
  const lowered = ` ${normalized.toLowerCase()} `;
119
157
  if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
120
- return { category: "blocked", reason: `Command contains blocked shell syntax: ${normalized}` };
158
+ return {
159
+ category: "general-shell",
160
+ needsNetwork: true,
161
+ writesWorkspace: true,
162
+ reason: `Command uses general shell syntax: ${normalized}`,
163
+ };
164
+ }
165
+ if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
166
+ const target = normalized.replace(/^mkdir\s+-p\s+/, "");
167
+ const virtualWorkspacePath = isSafeVirtualWorkspaceDir(target);
168
+ if (!isSafeRelativeDir(target) && !virtualWorkspacePath) {
169
+ return { category: "blocked", reason: `mkdir target must be a safe workspace-relative directory: ${target}` };
170
+ }
171
+ return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
121
172
  }
122
173
  if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
123
- return { category: "blocked", reason: `Command contains a write-capable or network token: ${normalized}` };
174
+ return {
175
+ category: "destructive",
176
+ needsNetwork: false,
177
+ writesWorkspace: true,
178
+ reason: `Command contains a write-capable or destructive token: ${normalized}`,
179
+ };
124
180
  }
125
181
 
126
182
  if (matchAny(READ_ONLY_PATTERNS, normalized)) {
@@ -132,6 +188,12 @@ export function classifyCommand(command) {
132
188
  if (matchAny(TOOLCHAIN_PATTERNS, normalized)) {
133
189
  return { category: "toolchain", needsNetwork: false, writesWorkspace: true };
134
190
  }
191
+ if (matchAny(NETWORK_FETCH_PATTERNS, normalized)) {
192
+ return { category: "network-fetch", needsNetwork: true, writesWorkspace: /(\s-o\s|\s-O\s)/.test(normalized) };
193
+ }
194
+ if (matchAny(SYSTEM_PACKAGE_INSTALL_PATTERNS, normalized)) {
195
+ return { category: "system-package-install", needsNetwork: true, writesWorkspace: false, requiresDockerRoot: true };
196
+ }
135
197
  if (matchAny(PACKAGE_INSTALL_PATTERNS, normalized)) {
136
198
  return { category: "package-install", needsNetwork: true, writesWorkspace: true };
137
199
  }
@@ -139,18 +201,57 @@ export function classifyCommand(command) {
139
201
  return { category: "env-setup", needsNetwork: false, writesWorkspace: true };
140
202
  }
141
203
 
142
- return { category: "blocked", reason: `Command is outside the execution allowlist: ${normalized}` };
204
+ return {
205
+ category: "general-shell",
206
+ needsNetwork: false,
207
+ writesWorkspace: false,
208
+ reason: `Command is outside the narrow allowlist and requires a trusted shell policy: ${normalized}`,
209
+ };
210
+ }
211
+
212
+ function classifyCdCommand(normalized) {
213
+ const match = normalized.match(/^cd\s+([-\w./]+)\s+&&\s+(.+)$/);
214
+ if (!match) return null;
215
+ const [, dir, inner] = match;
216
+ const virtualWorkspacePath = isSafeVirtualWorkspaceDir(dir);
217
+ if (!isSafeRelativeDir(dir) && !virtualWorkspacePath) {
218
+ return { category: "blocked", reason: `cd target must be a safe workspace-relative directory: ${dir}` };
219
+ }
220
+ const innerClassification = classifySimpleCommand(inner.trim());
221
+ if (innerClassification.category === "blocked") return innerClassification;
222
+ return { ...innerClassification, cdDir: dir, virtualWorkspacePath };
223
+ }
224
+
225
+ export function classifyCommand(command) {
226
+ const normalized = String(command || "").trim();
227
+ if (!normalized) return { category: "blocked", reason: "Command is empty." };
228
+
229
+ return classifyCdCommand(normalized) || classifySimpleCommand(normalized);
143
230
  }
144
231
 
145
232
  export function evaluateCommandPolicy(command, config) {
146
233
  const classification = classifyCommand(command);
147
234
  const sandboxMode = normalizeSandboxMode(config.sandboxMode);
148
235
  const packageInstallPolicy = normalizePackageInstallPolicy(config.packageInstallPolicy);
236
+ const dockerWorkspace = sandboxMode === "docker-workspace";
237
+ const packageInstallsAllowed = packageInstallPolicy === "allow";
238
+ const trustedDockerShell = dockerWorkspace && packageInstallsAllowed;
239
+ const trustedHostShell = sandboxMode === "host" && Boolean(config.allowDestructive);
149
240
 
150
241
  if (classification.category === "blocked") {
151
242
  return { allowed: false, ...classification, sandboxMode, packageInstallPolicy };
152
243
  }
153
244
 
245
+ if (classification.virtualWorkspacePath && !config.useDockerSandbox) {
246
+ return {
247
+ allowed: false,
248
+ ...classification,
249
+ reason: "Virtual /workspace shell paths are allowed only inside Docker sandbox mode.",
250
+ sandboxMode,
251
+ packageInstallPolicy,
252
+ };
253
+ }
254
+
154
255
  if (!config.allowShellTool) {
155
256
  return {
156
257
  allowed: false,
@@ -161,7 +262,11 @@ export function evaluateCommandPolicy(command, config) {
161
262
  };
162
263
  }
163
264
 
164
- if (classification.category === "package-install" || classification.category === "env-setup") {
265
+ if (
266
+ classification.category === "package-install" ||
267
+ classification.category === "env-setup" ||
268
+ classification.category === "system-package-install"
269
+ ) {
165
270
  if (packageInstallPolicy !== "allow") {
166
271
  return {
167
272
  allowed: false,
@@ -176,17 +281,53 @@ export function evaluateCommandPolicy(command, config) {
176
281
  };
177
282
  }
178
283
 
179
- if (sandboxMode !== "docker-workspace") {
284
+ if (classification.category === "system-package-install" && sandboxMode === "host" && !config.allowDestructive) {
180
285
  return {
181
286
  allowed: false,
182
287
  category: classification.category,
183
- reason: "Approved package/environment setup must run in Docker workspace-write sandbox mode.",
288
+ reason: "Host system package installs require Allow destructive actions. Prefer Docker workspace mode.",
184
289
  sandboxMode,
185
290
  packageInstallPolicy,
186
291
  };
187
292
  }
188
293
  }
189
294
 
295
+ if (classification.category === "general-shell" && !trustedDockerShell && !trustedHostShell) {
296
+ return {
297
+ allowed: false,
298
+ ...classification,
299
+ needsApproval: true,
300
+ reason:
301
+ sandboxMode === "host"
302
+ ? "General shell commands on the host require Allow destructive actions. Prefer Docker workspace mode for broad shell access."
303
+ : "General Docker shell commands require Package installs = allow in docker-workspace mode.",
304
+ sandboxMode,
305
+ packageInstallPolicy,
306
+ };
307
+ }
308
+
309
+ if (classification.category === "destructive" && !config.allowDestructive) {
310
+ return {
311
+ allowed: false,
312
+ ...classification,
313
+ needsApproval: true,
314
+ reason: "Destructive shell commands require Allow destructive actions.",
315
+ sandboxMode,
316
+ packageInstallPolicy,
317
+ };
318
+ }
319
+
320
+ if (classification.category === "network-fetch" && config.useDockerSandbox && !trustedDockerShell) {
321
+ return {
322
+ allowed: false,
323
+ ...classification,
324
+ needsApproval: true,
325
+ reason: "Docker network commands require docker-workspace mode with Package installs = allow.",
326
+ sandboxMode,
327
+ packageInstallPolicy,
328
+ };
329
+ }
330
+
190
331
  if (classification.writesWorkspace && config.useDockerSandbox && sandboxMode !== "docker-workspace") {
191
332
  return {
192
333
  allowed: false,
@@ -200,6 +341,7 @@ export function evaluateCommandPolicy(command, config) {
200
341
  return {
201
342
  allowed: true,
202
343
  ...classification,
344
+ requiresDockerRoot: Boolean(classification.requiresDockerRoot && config.useDockerSandbox),
203
345
  sandboxMode,
204
346
  packageInstallPolicy,
205
347
  };
package/src/config.js CHANGED
@@ -71,7 +71,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
71
71
  ? overrides.allowedDomains
72
72
  : parseList(process.env.ALLOWED_DOMAINS),
73
73
  allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
74
- allowDestructive: parseBoolean(overrides.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
74
+ allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
75
75
  allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, false),
76
76
  allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
77
77
  allowWrapperTools: parseBoolean(
@@ -168,7 +168,7 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
168
168
  function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config)) {
169
169
  const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
170
170
  const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
171
- const userArgs = uid && gid ? ["--user", `${uid}:${gid}`] : [];
171
+ const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
172
172
  const sandboxMode = normalizeSandboxMode(config.sandboxMode);
173
173
  const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
174
174
  const networkMode = policy.needsNetwork ? "bridge" : "none";
@@ -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
- "When the user asks to draw, plot, graph, chart, diagram, create a figure, or visualize something, include a canvas artifact even if the user does not mention canvas. Prefer a small SVG file or concise markdown figure when file tools are available.",
160
- "When the user asks for LaTeX, TeX, a paper, manuscript, report, or PDF, plan to create the needed source/assets, compile with the available allowlisted TeX toolchain, and publish the PDF through the canvas tunnel. For subfolder documents, keep outputs beside the source. For generated figures, use pdflatex-compatible 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, 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)
@@ -298,7 +300,7 @@ export async function requestNextStep(client, config, messages) {
298
300
  function: {
299
301
  name: "run_command",
300
302
  description:
301
- "Run an allowlisted terminal command in the configured working directory. Good for inspection, tests, and approved Docker package/environment setup. NPM publishing, tokens, sudo, arbitrary network commands, and destructive git/file actions are blocked.",
303
+ "Run a terminal command in the configured working directory under the active shell policy. Secrets and npm publishing are always blocked; Docker workspace mode with approved package installs supports broader network/setup commands, while host destructive or privileged work requires explicit trust.",
302
304
  parameters: {
303
305
  type: "object",
304
306
  properties: {
@@ -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: inspect files first, make targeted workspace-local edits, run relevant checks, and report changed files and residual risks.",
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, inspect package scripts, use npm checks/tests when safe, and keep generated files inside the project workspace.",
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 local check when useful, and use the configured sandbox/package policy for dependencies.",
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 package workflows. Prefer project-local config, safe publish preparation, and explicit secret handling.",
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 idempotent scripts, ask for approval before privileged or destructive operations, and avoid leaking credentials.",
76
+ "For system maintenance, diagnose first, use Docker for broad installs when available, and follow the configured trust/package policy for host-level changes.",
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 || "");