@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.79",
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",
@@ -780,12 +780,21 @@ async function implicitOverwriteBlock(toolName, args, config, state) {
780
780
  };
781
781
  }
782
782
 
783
- function sanitizeToolResult(result) {
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
- safeResult.contentPreview = safeResult.content.slice(0, 600);
787
- safeResult.contentBytes = Buffer.byteLength(safeResult.content, "utf8");
788
- delete safeResult.content;
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
  }
@@ -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) {
@@ -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: {