@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 +1 -1
- package/scripts/smoke-coding-tools.js +25 -0
- package/src/command-policy.js +121 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
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",
|
package/src/command-policy.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
399
|
-
|
|
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) {
|