@lazyingart/agintiflow 0.20.87 → 0.20.89

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.87",
3
+ "version": "0.20.89",
4
4
  "type": "module",
5
5
  "description": "Low-cost, project-aware Web and CLI agents with DeepSeek/Venice/OpenAI routing, visible tool calls, durable sessions, scouts, AAPS, SCS, and guarded local execution.",
6
6
  "license": "Apache-2.0",
@@ -266,6 +266,28 @@ try {
266
266
  packageInstallPolicy: "allow",
267
267
  commandCwd: workspace,
268
268
  };
269
+ const dockerWorkspaceNoInstallsPolicy = {
270
+ ...dockerWorkspacePolicy,
271
+ packageInstallPolicy: "block",
272
+ };
273
+ const readonlyProbePolicy = evaluateCommandPolicy(
274
+ 'which pdflatex 2>&1; which latexmk 2>&1; echo "---"; find /workspace -name \'*.tex\' -maxdepth 3 2>/dev/null; echo "exit: $?"',
275
+ dockerWorkspaceNoInstallsPolicy
276
+ );
277
+ assert(readonlyProbePolicy.allowed, "read-only toolchain probe sequence should not require package-install-policy=allow");
278
+ assert(readonlyProbePolicy.category === "read-only", "read-only toolchain probe sequence should be classified as read-only");
279
+ const readonlyVersionPipelinePolicy = evaluateCommandPolicy(
280
+ "which pdflatex latexmk python3 2>&1; pdflatex --version 2>&1 | head -2; latexmk --version 2>&1 | head -2",
281
+ dockerWorkspaceNoInstallsPolicy
282
+ );
283
+ assert(readonlyVersionPipelinePolicy.allowed, "read-only version probe pipelines should not require package-install-policy=allow");
284
+ assert(readonlyVersionPipelinePolicy.category === "read-only", "read-only version probe pipelines should be classified as read-only");
285
+ const readonlyTestEchoPolicy = evaluateCommandPolicy(
286
+ 'test -f /usr/bin/pdflatex && echo "pdflatex: FOUND" || echo "pdflatex: NOT FOUND"; test -f /usr/bin/latexmk && echo "latexmk: FOUND" || echo "latexmk: NOT FOUND"; python3 --version 2>&1',
287
+ dockerWorkspaceNoInstallsPolicy
288
+ );
289
+ assert(readonlyTestEchoPolicy.allowed, "read-only test/echo probe sequence should not require package-install-policy=allow");
290
+ assert(readonlyTestEchoPolicy.category === "read-only", "read-only test/echo probe sequence should be classified as read-only");
269
291
  const curlPolicy = evaluateCommandPolicy("curl -s -o /dev/null -w '%{http_code}' https://github.com/lazyingart/AgInTiFlow.git", dockerWorkspacePolicy);
270
292
  assert(curlPolicy.allowed, "curl URL probe with flags should be allowed in docker-workspace allow mode");
271
293
  assert(curlPolicy.needsNetwork, "curl URL probe with flags was not classified as network");
@@ -725,6 +747,9 @@ try {
725
747
  "large_profile_pro_route",
726
748
  "auto_system_pro_route",
727
749
  "auto_engineering_guidance",
750
+ "command_policy_readonly_probe_sequence_no_installs",
751
+ "command_policy_readonly_version_pipeline_no_installs",
752
+ "command_policy_readonly_test_echo_no_installs",
728
753
  "command_policy_git_clone_network",
729
754
  "command_policy_safe_chmod_sequence",
730
755
  "command_policy_cd_workspace",
@@ -23,9 +23,32 @@ const READ_ONLY_PATTERNS = [
23
23
  /^python(?:3)?\s+--version$/,
24
24
  /^pip(?:3)?\s+--version$/,
25
25
  /^conda\s+--version$/,
26
+ /^(?:pdflatex|latexmk)\s+--version$/,
27
+ /^test\s+-[efdx]\s+[-\w./~]+$/,
28
+ /^true$/,
29
+ /^false$/,
26
30
  /^echo(?:\s+.+)?$/,
27
31
  ];
28
32
 
33
+ function stripBenignRedirections(command = "") {
34
+ return String(command || "")
35
+ .replace(/\s+2>&1\b/g, "")
36
+ .replace(/\s+1>&2\b/g, "")
37
+ .replace(/\s+2>\/dev\/null\b/g, "")
38
+ .replace(/\s+1>\/dev\/null\b/g, "")
39
+ .replace(/\s+>\/dev\/null\b/g, "")
40
+ .trim();
41
+ }
42
+
43
+ function isReadOnlyFindCommand(command = "") {
44
+ const normalized = stripBenignRedirections(command);
45
+ if (!/^find\s+/.test(normalized)) return false;
46
+ if (/(^|\s)(-delete|-exec|-execdir|-ok|-okdir|-fprint|-fprintf|-fls)\b/.test(normalized)) return false;
47
+ const unquoted = stripQuotedSegments(normalized);
48
+ if (/[|<>;&`$]/.test(unquoted)) return false;
49
+ return /^find\s+(?:[-./~\w]+|\/workspace)(?:\s+[-\w]+(?:\s+(?:"[^"\n]*"|'[^'\n]*'|[^\s|<>;&`$]+))?)*$/.test(normalized);
50
+ }
51
+
29
52
  const TEST_PATTERNS = [
30
53
  /^npm\s+(run\s+)?(check|test|build|lint)(?:\s+--\s+[-\w./:=]+)*$/,
31
54
  /^npm\s+--prefix\s+[-\w./]+\s+(run\s+)?(check|test|build|lint)(?:\s+--\s+[-\w./:=]+)*$/,
@@ -242,6 +265,7 @@ function classifyGitClone(normalized) {
242
265
  }
243
266
 
244
267
  function classifySimpleCommand(normalized) {
268
+ const readOnlyCommand = stripBenignRedirections(normalized);
245
269
  if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
246
270
  return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
247
271
  }
@@ -294,7 +318,7 @@ function classifySimpleCommand(normalized) {
294
318
  const gitCloneClassification = classifyGitClone(normalized);
295
319
  if (gitCloneClassification) return gitCloneClassification;
296
320
 
297
- const unquoted = stripQuotedSegments(normalized);
321
+ const unquoted = stripQuotedSegments(readOnlyCommand);
298
322
  const lowered = ` ${unquoted.toLowerCase()} `;
299
323
  if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
300
324
  return {
@@ -313,7 +337,7 @@ function classifySimpleCommand(normalized) {
313
337
  };
314
338
  }
315
339
 
316
- if (matchAny(READ_ONLY_PATTERNS, normalized)) {
340
+ if (matchAny(READ_ONLY_PATTERNS, readOnlyCommand) || isReadOnlyFindCommand(normalized)) {
317
341
  return { category: "read-only", needsNetwork: false, writesWorkspace: false };
318
342
  }
319
343
  if (matchAny(TEST_PATTERNS, normalized)) {
@@ -372,13 +396,13 @@ function splitTopLevelShellSequence(command = "") {
372
396
  quote = char;
373
397
  continue;
374
398
  }
375
- if (char === ";" || (char === "&" && text[index + 1] === "&")) {
399
+ if (char === ";" || (char === "&" && text[index + 1] === "&") || (char === "|" && text[index + 1] === "|")) {
376
400
  const part = current.trim();
377
401
  if (!part) return null;
378
402
  parts.push(part);
379
403
  current = "";
380
404
  hadSeparator = true;
381
- if (char === "&") index += 1;
405
+ if (char === "&" || char === "|") index += 1;
382
406
  continue;
383
407
  }
384
408
  current += char;
@@ -392,17 +416,105 @@ function splitTopLevelShellSequence(command = "") {
392
416
  function classifyShellSequence(normalized) {
393
417
  const parts = splitTopLevelShellSequence(normalized);
394
418
  if (!parts) return null;
395
- const classifications = parts.map((part) => classifyCdCommand(part) || classifySimpleCommand(part));
419
+ const classifications = parts.map((part) => classifyCdCommand(part) || classifyPipelineSequence(part) || classifySimpleCommand(part));
396
420
  const blocked = classifications.find((classification) => classification.category === "blocked" || classification.category === "destructive");
397
421
  if (blocked) return blocked;
398
- return {
399
- category: "general-shell",
422
+ const broad = classifications.find((classification) => classification.category === "general-shell");
423
+ if (broad) {
424
+ return {
425
+ ...broad,
426
+ reason: `Command sequence includes a broad shell segment and requires trusted shell policy: ${normalized}`,
427
+ };
428
+ }
429
+ const categories = new Set(classifications.map((classification) => classification.category));
430
+ const aggregate = {
400
431
  needsNetwork: classifications.some((classification) => classification.needsNetwork),
401
432
  writesWorkspace: classifications.some((classification) => classification.writesWorkspace),
402
433
  requiresDockerRoot: classifications.some((classification) => classification.requiresDockerRoot),
403
434
  virtualWorkspacePath: classifications.some((classification) => classification.virtualWorkspacePath),
404
435
  reason: `Command sequence uses shell separators with individually classified safe segments: ${normalized}`,
405
436
  };
437
+ if (categories.has("system-package-install")) return { category: "system-package-install", ...aggregate };
438
+ if (categories.has("package-install")) return { category: "package-install", ...aggregate };
439
+ if (categories.has("env-setup")) return { category: "env-setup", ...aggregate };
440
+ if (categories.has("permission-change")) return { category: "permission-change", ...aggregate };
441
+ if (categories.has("git-remote")) return { category: "git-remote", ...aggregate };
442
+ if (categories.has("network-fetch")) return { category: "network-fetch", ...aggregate };
443
+ if (categories.has("workspace-write")) return { category: "workspace-write", ...aggregate };
444
+ if (categories.has("toolchain")) return { category: "toolchain", ...aggregate };
445
+ if (categories.has("test")) return { category: "test", ...aggregate };
446
+ if (categories.has("git-workflow")) return { category: "git-workflow", ...aggregate };
447
+ return {
448
+ category: "read-only",
449
+ ...aggregate,
450
+ };
451
+ }
452
+
453
+ function splitTopLevelPipeline(command = "") {
454
+ const parts = [];
455
+ let current = "";
456
+ let quote = "";
457
+ let escaped = false;
458
+ let hadPipe = false;
459
+ const text = String(command || "");
460
+ for (let index = 0; index < text.length; index += 1) {
461
+ const char = text[index];
462
+ if (escaped) {
463
+ current += char;
464
+ escaped = false;
465
+ continue;
466
+ }
467
+ if (char === "\\") {
468
+ current += char;
469
+ escaped = true;
470
+ continue;
471
+ }
472
+ if (quote) {
473
+ current += char;
474
+ if (char === quote) quote = "";
475
+ continue;
476
+ }
477
+ if (char === "'" || char === '"') {
478
+ current += char;
479
+ quote = char;
480
+ continue;
481
+ }
482
+ if (char === "|" && text[index + 1] !== "|") {
483
+ const part = current.trim();
484
+ if (!part) return null;
485
+ parts.push(part);
486
+ current = "";
487
+ hadPipe = true;
488
+ continue;
489
+ }
490
+ current += char;
491
+ }
492
+ const finalPart = current.trim();
493
+ if (finalPart) parts.push(finalPart);
494
+ if (!hadPipe || parts.length < 2) return null;
495
+ return parts;
496
+ }
497
+
498
+ function classifyPipelineSequence(normalized) {
499
+ const parts = splitTopLevelPipeline(normalized);
500
+ if (!parts) return null;
501
+ const classifications = parts.map((part) => classifyCdCommand(part) || classifySimpleCommand(part));
502
+ const blocked = classifications.find((classification) => classification.category === "blocked" || classification.category === "destructive");
503
+ if (blocked) return blocked;
504
+ if (classifications.every((classification) => classification.category === "read-only")) {
505
+ return {
506
+ category: "read-only",
507
+ needsNetwork: false,
508
+ writesWorkspace: false,
509
+ reason: `Read-only shell pipeline: ${normalized}`,
510
+ };
511
+ }
512
+ return {
513
+ category: "general-shell",
514
+ needsNetwork: classifications.some((classification) => classification.needsNetwork),
515
+ writesWorkspace: classifications.some((classification) => classification.writesWorkspace),
516
+ reason: `Shell pipeline includes a broad segment and requires trusted shell policy: ${normalized}`,
517
+ };
406
518
  }
407
519
 
408
520
  function classifyCdCommand(normalized) {
@@ -413,7 +525,7 @@ function classifyCdCommand(normalized) {
413
525
  if (!isSafeRelativeDir(dir) && !virtualWorkspacePath) {
414
526
  return { category: "blocked", reason: `cd target must be a safe workspace-relative directory: ${dir}` };
415
527
  }
416
- const innerClassification = classifySimpleCommand(inner.trim());
528
+ const innerClassification = classifyShellSequence(inner.trim()) || classifyPipelineSequence(inner.trim()) || classifySimpleCommand(inner.trim());
417
529
  if (innerClassification.category === "blocked") return innerClassification;
418
530
  return { ...innerClassification, cdDir: dir, virtualWorkspacePath };
419
531
  }
@@ -422,7 +534,7 @@ export function classifyCommand(command) {
422
534
  const normalized = String(command || "").trim();
423
535
  if (!normalized) return { category: "blocked", reason: "Command is empty." };
424
536
 
425
- return classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifySimpleCommand(normalized);
537
+ return classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifyPipelineSequence(normalized) || classifySimpleCommand(normalized);
426
538
  }
427
539
 
428
540
  export function evaluateCommandPolicy(command, config) {