@lazyingart/agintiflow 0.20.79 → 0.20.80
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 +54 -1
- package/src/agent-runner.js +13 -4
- package/src/command-policy.js +95 -1
- package/src/model-client.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.80",
|
|
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",
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { repairModelMessageHistory, runAgent, shouldShortCircuitToolBatch, skippedAfterBlockedToolResult } from "../src/agent-runner.js";
|
|
6
|
+
import { repairModelMessageHistory, runAgent, sanitizeToolResult, shouldShortCircuitToolBatch, skippedAfterBlockedToolResult } from "../src/agent-runner.js";
|
|
7
7
|
import { formatBehaviorContractForPrompt } from "../src/behavior-contract.js";
|
|
8
8
|
import { resolveRuntimeConfig } from "../src/config.js";
|
|
9
9
|
import { readCodebaseMap } from "../src/codebase-map.js";
|
|
@@ -285,6 +285,17 @@ try {
|
|
|
285
285
|
const actualDangerAfterQuotePolicy = evaluateCommandPolicy('echo "rm -rf is text" && rm -rf reports', dockerWorkspacePolicy);
|
|
286
286
|
assert(!actualDangerAfterQuotePolicy.allowed, "actual destructive command after quoted text should still be blocked");
|
|
287
287
|
assert(actualDangerAfterQuotePolicy.category === "destructive", "actual destructive command after quoted text was not classified as destructive");
|
|
288
|
+
const safeChmodAndRunPolicy = evaluateCommandPolicy(
|
|
289
|
+
'chmod +x /workspace/reports/run_bounded_02079_v2.sh && bash /workspace/reports/run_bounded_02079.sh 2>&1; echo "RUN_COMMAND_EXIT: $?"',
|
|
290
|
+
dockerWorkspacePolicy
|
|
291
|
+
);
|
|
292
|
+
assert(safeChmodAndRunPolicy.allowed, "safe workspace chmod + script run sequence should be allowed in docker-workspace allow mode");
|
|
293
|
+
const unsafeChmodPolicy = evaluateCommandPolicy("chmod +x /etc/passwd", dockerWorkspacePolicy);
|
|
294
|
+
assert(!unsafeChmodPolicy.allowed, "chmod outside the workspace should be blocked");
|
|
295
|
+
const cdWorkspacePolicy = evaluateCommandPolicy("cd /workspace && git status --short 2>&1 | head -20", dockerWorkspacePolicy);
|
|
296
|
+
assert(cdWorkspacePolicy.allowed, "cd /workspace should be allowed in docker-workspace mode");
|
|
297
|
+
const gitCleanDryRunPolicy = evaluateCommandPolicy("git clean -nd reports", dockerWorkspacePolicy);
|
|
298
|
+
assert(gitCleanDryRunPolicy.allowed, "git clean dry-run should be allowed as read-only inspection evidence");
|
|
288
299
|
const unsafeCloneTarget = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git ../AgInTiFlow", dockerWorkspacePolicy);
|
|
289
300
|
assert(!unsafeCloneTarget.allowed, "git clone outside the workspace should be blocked");
|
|
290
301
|
const blockedClonePolicy = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git", {
|
|
@@ -401,6 +412,44 @@ try {
|
|
|
401
412
|
);
|
|
402
413
|
await fs.writeFile(path.join(workspace, "src/index.js"), "export function answer() { return 42; }\n", "utf8");
|
|
403
414
|
await fs.writeFile(path.join(workspace, "test/index.test.js"), "import test from 'node:test';\n", "utf8");
|
|
415
|
+
const longSmallFile = [
|
|
416
|
+
"# Small file read smoke",
|
|
417
|
+
"line 001",
|
|
418
|
+
"line 002",
|
|
419
|
+
"line 003",
|
|
420
|
+
"line 004",
|
|
421
|
+
"line 005",
|
|
422
|
+
"line 006",
|
|
423
|
+
"line 007",
|
|
424
|
+
"line 008",
|
|
425
|
+
"line 009",
|
|
426
|
+
"line 010",
|
|
427
|
+
"line 011",
|
|
428
|
+
"line 012",
|
|
429
|
+
"line 013",
|
|
430
|
+
"line 014",
|
|
431
|
+
"line 015",
|
|
432
|
+
"line 016",
|
|
433
|
+
"line 017",
|
|
434
|
+
"line 018",
|
|
435
|
+
"line 019",
|
|
436
|
+
"line 020",
|
|
437
|
+
"FINAL_SENTINEL_SMALL_FILE_FULL_CONTENT",
|
|
438
|
+
"",
|
|
439
|
+
].join("\n");
|
|
440
|
+
await fs.writeFile(path.join(workspace, "small-read-smoke.md"), longSmallFile, "utf8");
|
|
441
|
+
const smallReadResult = await executeWorkspaceTool(
|
|
442
|
+
"read_file",
|
|
443
|
+
{ path: "small-read-smoke.md" },
|
|
444
|
+
{
|
|
445
|
+
commandCwd: workspace,
|
|
446
|
+
allowFileTools: true,
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
const sanitizedSmallRead = sanitizeToolResult(smallReadResult);
|
|
450
|
+
assert(sanitizedSmallRead.content === longSmallFile, "small read_file result did not keep full content for the model");
|
|
451
|
+
assert(sanitizedSmallRead.contentTruncated === false, "small read_file result should not be marked truncated");
|
|
452
|
+
assert(!("contentPreview" in sanitizedSmallRead), "small read_file result should not replace full content with preview");
|
|
404
453
|
const inspected = await executeWorkspaceTool(
|
|
405
454
|
"inspect_project",
|
|
406
455
|
{ path: ".", maxDepth: 4, limit: 200 },
|
|
@@ -677,12 +726,16 @@ try {
|
|
|
677
726
|
"auto_system_pro_route",
|
|
678
727
|
"auto_engineering_guidance",
|
|
679
728
|
"command_policy_git_clone_network",
|
|
729
|
+
"command_policy_safe_chmod_sequence",
|
|
730
|
+
"command_policy_cd_workspace",
|
|
731
|
+
"command_policy_git_clean_dry_run",
|
|
680
732
|
"permission_recovery_advice",
|
|
681
733
|
"parallel_scout_trigger",
|
|
682
734
|
"parallel_scout_roster",
|
|
683
735
|
"parallel_scout_count_clamp",
|
|
684
736
|
"web_search_dry_run",
|
|
685
737
|
"inspect_project",
|
|
738
|
+
"small_read_file_full_content",
|
|
686
739
|
"parallel_scout_context_pack",
|
|
687
740
|
"durable_codebase_map",
|
|
688
741
|
"scout_blackboard",
|
package/src/agent-runner.js
CHANGED
|
@@ -780,12 +780,21 @@ async function implicitOverwriteBlock(toolName, args, config, state) {
|
|
|
780
780
|
};
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
-
|
|
783
|
+
const TOOL_RESULT_INLINE_CONTENT_BYTES = 16_000;
|
|
784
|
+
const TOOL_RESULT_CONTENT_PREVIEW_CHARS = 1_200;
|
|
785
|
+
|
|
786
|
+
export function sanitizeToolResult(result) {
|
|
784
787
|
const safeResult = redactValue(result);
|
|
785
788
|
if (typeof safeResult.content === "string") {
|
|
786
|
-
|
|
787
|
-
safeResult.contentBytes =
|
|
788
|
-
|
|
789
|
+
const contentBytes = Buffer.byteLength(safeResult.content, "utf8");
|
|
790
|
+
safeResult.contentBytes = contentBytes;
|
|
791
|
+
if (safeResult.toolName === "read_file" && contentBytes <= TOOL_RESULT_INLINE_CONTENT_BYTES) {
|
|
792
|
+
safeResult.contentTruncated = false;
|
|
793
|
+
} else {
|
|
794
|
+
safeResult.contentPreview = safeResult.content.slice(0, TOOL_RESULT_CONTENT_PREVIEW_CHARS);
|
|
795
|
+
safeResult.contentTruncated = true;
|
|
796
|
+
delete safeResult.content;
|
|
797
|
+
}
|
|
789
798
|
}
|
|
790
799
|
return safeResult;
|
|
791
800
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -190,10 +190,34 @@ function isSafeRelativeDir(value) {
|
|
|
190
190
|
|
|
191
191
|
function isSafeVirtualWorkspaceDir(value) {
|
|
192
192
|
const normalized = String(value || "").trim();
|
|
193
|
+
if (normalized === "/workspace") return true;
|
|
193
194
|
if (!normalized.startsWith("/workspace/")) return false;
|
|
194
195
|
return isSafeRelativeDir(normalized.replace(/^\/workspace\//, ""));
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
function isSafeVirtualWorkspacePath(value) {
|
|
199
|
+
const normalized = String(value || "").trim();
|
|
200
|
+
return normalized.startsWith("/workspace/") && isSafeRelativeDir(normalized.replace(/^\/workspace\//, ""));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isSafeWorkspacePath(value) {
|
|
204
|
+
return isSafeRelativeDir(value) || isSafeVirtualWorkspacePath(value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function classifyGitCleanDryRun(normalized) {
|
|
208
|
+
const match = normalized.match(/^git\s+clean\b([\s\S]*)$/);
|
|
209
|
+
if (!match) return null;
|
|
210
|
+
const args = match[1] || "";
|
|
211
|
+
if (!/(^|\s)(?:-n\b|--dry-run\b|-[A-Za-z]*n[A-Za-z]*\b)/.test(args)) return null;
|
|
212
|
+
if (/(^|\s)(?:-f\b|--force\b|-[A-Za-z]*f[A-Za-z]*\b)/.test(args)) return null;
|
|
213
|
+
return {
|
|
214
|
+
category: "read-only",
|
|
215
|
+
needsNetwork: false,
|
|
216
|
+
writesWorkspace: false,
|
|
217
|
+
reason: "Git clean dry-run is read-only inspection evidence.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
197
221
|
function classifyGitClone(normalized) {
|
|
198
222
|
const match = normalized.match(
|
|
199
223
|
/^git\s+clone(?:\s+--depth\s+\d+)?(?:\s+--branch\s+[-\w./]+)?\s+(https:\/\/\S+)(?:\s+([-\w./]+))?$/
|
|
@@ -224,6 +248,8 @@ function classifySimpleCommand(normalized) {
|
|
|
224
248
|
if (SENSITIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
225
249
|
return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
|
|
226
250
|
}
|
|
251
|
+
const gitCleanDryRun = classifyGitCleanDryRun(normalized);
|
|
252
|
+
if (gitCleanDryRun) return gitCleanDryRun;
|
|
227
253
|
if (matchAny(UNSAFE_GIT_PATTERNS, normalized)) {
|
|
228
254
|
return {
|
|
229
255
|
category: "destructive",
|
|
@@ -242,10 +268,16 @@ function classifySimpleCommand(normalized) {
|
|
|
242
268
|
return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
|
|
243
269
|
}
|
|
244
270
|
if (matchAny(PERMISSION_CHANGE_PATTERNS, normalized)) {
|
|
271
|
+
const target = normalized.split(/\s+/).at(-1) || "";
|
|
272
|
+
const virtualWorkspacePath = isSafeVirtualWorkspacePath(target);
|
|
273
|
+
if (!isSafeWorkspacePath(target)) {
|
|
274
|
+
return { category: "blocked", reason: `chmod target must be a safe workspace-relative path: ${target}` };
|
|
275
|
+
}
|
|
245
276
|
return {
|
|
246
277
|
category: "permission-change",
|
|
247
278
|
needsNetwork: false,
|
|
248
279
|
writesWorkspace: true,
|
|
280
|
+
virtualWorkspacePath,
|
|
249
281
|
reason: `Command changes workspace file mode: ${normalized}`,
|
|
250
282
|
};
|
|
251
283
|
}
|
|
@@ -311,6 +343,68 @@ function classifySimpleCommand(normalized) {
|
|
|
311
343
|
};
|
|
312
344
|
}
|
|
313
345
|
|
|
346
|
+
function splitTopLevelShellSequence(command = "") {
|
|
347
|
+
const parts = [];
|
|
348
|
+
let current = "";
|
|
349
|
+
let quote = "";
|
|
350
|
+
let escaped = false;
|
|
351
|
+
let hadSeparator = false;
|
|
352
|
+
const text = String(command || "");
|
|
353
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
354
|
+
const char = text[index];
|
|
355
|
+
if (escaped) {
|
|
356
|
+
current += char;
|
|
357
|
+
escaped = false;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (char === "\\") {
|
|
361
|
+
current += char;
|
|
362
|
+
escaped = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (quote) {
|
|
366
|
+
current += char;
|
|
367
|
+
if (char === quote) quote = "";
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (char === "'" || char === '"') {
|
|
371
|
+
current += char;
|
|
372
|
+
quote = char;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (char === ";" || (char === "&" && text[index + 1] === "&")) {
|
|
376
|
+
const part = current.trim();
|
|
377
|
+
if (!part) return null;
|
|
378
|
+
parts.push(part);
|
|
379
|
+
current = "";
|
|
380
|
+
hadSeparator = true;
|
|
381
|
+
if (char === "&") index += 1;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
current += char;
|
|
385
|
+
}
|
|
386
|
+
const finalPart = current.trim();
|
|
387
|
+
if (finalPart) parts.push(finalPart);
|
|
388
|
+
if (!hadSeparator || parts.length < 2) return null;
|
|
389
|
+
return parts;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function classifyShellSequence(normalized) {
|
|
393
|
+
const parts = splitTopLevelShellSequence(normalized);
|
|
394
|
+
if (!parts) return null;
|
|
395
|
+
const classifications = parts.map((part) => classifyCdCommand(part) || classifySimpleCommand(part));
|
|
396
|
+
const blocked = classifications.find((classification) => classification.category === "blocked" || classification.category === "destructive");
|
|
397
|
+
if (blocked) return blocked;
|
|
398
|
+
return {
|
|
399
|
+
category: "general-shell",
|
|
400
|
+
needsNetwork: classifications.some((classification) => classification.needsNetwork),
|
|
401
|
+
writesWorkspace: classifications.some((classification) => classification.writesWorkspace),
|
|
402
|
+
requiresDockerRoot: classifications.some((classification) => classification.requiresDockerRoot),
|
|
403
|
+
virtualWorkspacePath: classifications.some((classification) => classification.virtualWorkspacePath),
|
|
404
|
+
reason: `Command sequence uses shell separators with individually classified safe segments: ${normalized}`,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
314
408
|
function classifyCdCommand(normalized) {
|
|
315
409
|
const match = normalized.match(/^cd\s+([-\w./]+)\s+&&\s+(.+)$/);
|
|
316
410
|
if (!match) return null;
|
|
@@ -328,7 +422,7 @@ export function classifyCommand(command) {
|
|
|
328
422
|
const normalized = String(command || "").trim();
|
|
329
423
|
if (!normalized) return { category: "blocked", reason: "Command is empty." };
|
|
330
424
|
|
|
331
|
-
return classifyCdCommand(normalized) || classifySimpleCommand(normalized);
|
|
425
|
+
return classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifySimpleCommand(normalized);
|
|
332
426
|
}
|
|
333
427
|
|
|
334
428
|
export function evaluateCommandPolicy(command, config) {
|
package/src/model-client.js
CHANGED
|
@@ -887,7 +887,7 @@ export async function requestNextStep(client, config, messages) {
|
|
|
887
887
|
function: {
|
|
888
888
|
name: "read_file",
|
|
889
889
|
description:
|
|
890
|
-
"Read a small UTF-8 workspace file. Secret paths, .git internals, files outside the workspace, binary files, and huge files are blocked.",
|
|
890
|
+
"Read a small UTF-8 workspace file. Small files return full content; larger files return a truncated preview with contentTruncated=true, so do not quote or reproduce omitted content unless you read it another way. Secret paths, .git internals, files outside the workspace, binary files, and huge files are blocked.",
|
|
891
891
|
parameters: {
|
|
892
892
|
type: "object",
|
|
893
893
|
properties: {
|