@lazyingart/agintiflow 0.20.78 → 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.78",
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,8 @@ 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
+ import { formatBehaviorContractForPrompt } from "../src/behavior-contract.js";
7
8
  import { resolveRuntimeConfig } from "../src/config.js";
8
9
  import { readCodebaseMap } from "../src/codebase-map.js";
9
10
  import { evaluateCommandPolicy } from "../src/command-policy.js";
@@ -249,6 +250,15 @@ try {
249
250
  guidance.includes("find . -type d -name __pycache__"),
250
251
  "engineering guidance did not include recursive Python transient checks"
251
252
  );
253
+ const behaviorContract = formatBehaviorContractForPrompt();
254
+ assert(
255
+ behaviorContract.includes("For tmux one-shot jobs"),
256
+ "behavior contract did not include tmux one-shot evidence guidance"
257
+ );
258
+ assert(
259
+ behaviorContract.includes("do not claim stdout, stderr, or exit status"),
260
+ "behavior contract did not forbid inferring tmux output after capture failure"
261
+ );
252
262
  const dockerWorkspacePolicy = {
253
263
  allowShellTool: true,
254
264
  useDockerSandbox: true,
@@ -275,6 +285,17 @@ try {
275
285
  const actualDangerAfterQuotePolicy = evaluateCommandPolicy('echo "rm -rf is text" && rm -rf reports', dockerWorkspacePolicy);
276
286
  assert(!actualDangerAfterQuotePolicy.allowed, "actual destructive command after quoted text should still be blocked");
277
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");
278
299
  const unsafeCloneTarget = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git ../AgInTiFlow", dockerWorkspacePolicy);
279
300
  assert(!unsafeCloneTarget.allowed, "git clone outside the workspace should be blocked");
280
301
  const blockedClonePolicy = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git", {
@@ -391,6 +412,44 @@ try {
391
412
  );
392
413
  await fs.writeFile(path.join(workspace, "src/index.js"), "export function answer() { return 42; }\n", "utf8");
393
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");
394
453
  const inspected = await executeWorkspaceTool(
395
454
  "inspect_project",
396
455
  { path: ".", maxDepth: 4, limit: 200 },
@@ -667,12 +726,16 @@ try {
667
726
  "auto_system_pro_route",
668
727
  "auto_engineering_guidance",
669
728
  "command_policy_git_clone_network",
729
+ "command_policy_safe_chmod_sequence",
730
+ "command_policy_cd_workspace",
731
+ "command_policy_git_clean_dry_run",
670
732
  "permission_recovery_advice",
671
733
  "parallel_scout_trigger",
672
734
  "parallel_scout_roster",
673
735
  "parallel_scout_count_clamp",
674
736
  "web_search_dry_run",
675
737
  "inspect_project",
738
+ "small_read_file_full_content",
676
739
  "parallel_scout_context_pack",
677
740
  "durable_codebase_map",
678
741
  "scout_blackboard",
@@ -30,5 +30,6 @@ Workflow:
30
30
  3. Use `tmux_start_session` for new durable host jobs rooted in the workspace.
31
31
  4. Use `tmux_send_keys` sparingly and never send secrets, sudo passwords, destructive commands, or unreviewed pasted scripts.
32
32
  5. For long commands, capture progress periodically and summarize the latest useful lines instead of flooding the chat.
33
+ 6. For one-shot tmux jobs, redirect stdout, stderr, and exit status to a durable workspace log or keep the shell open long enough for `tmux_capture_pane`. If capture fails because the session already ended, do not infer output or exit status; say the tmux output is unavailable and rely only on separately verified evidence.
33
34
 
34
35
  If host tmux is unavailable, report that limitation and suggest installing tmux on the host or using a future persistent service-container mode. If a package or sudo install is missing, report the exact command and whether it should run in Docker, host, or a user-owned tmux session.
@@ -428,8 +428,9 @@ async function createInitialState(config, sessionId) {
428
428
  "Permission contract: current-workspace file writes are allowed through workspace file tools when enabled. Outside-workspace paths, host sudo, host OS package installs, destructive git/shell actions, and blocked network/setup must not be bypassed by retrying variants. If a tool result includes permissionAdvice or suggestedCommand, stop, explain the blocker, copy the exact suggestedCommand when giving a rerun path, and ask the user to approve/rerun that mode or choose a safer workspace-relative path. Never invent legacy AgInTi syntax such as `aginti run --sandbox host`; use the exact flags from permissionAdvice.",
429
429
  "If an operation fails but a directory, artifact, or file already exists, treat it as pre-existing unless you have evidence this run created or updated it. Verify expected outputs before claiming success.",
430
430
  config.allowShellTool
431
- ? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived."
432
- : "",
431
+ ? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived."
432
+ + " For one-shot tmux commands, redirect stdout/stderr and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
433
+ : "",
433
434
  config.allowFileTools
434
435
  ? `Workspace file tools are available in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. For large or unfamiliar repositories, call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests as relevant before editing. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. For newly generated standalone prose/docs/stories/assets, choose a descriptive non-conflicting filename from the topic/language and use mode=create; do not overwrite existing files unless the user explicitly asked to update/replace/overwrite that file. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
435
436
  : "No workspace file tools are available.",
@@ -779,12 +780,21 @@ async function implicitOverwriteBlock(toolName, args, config, state) {
779
780
  };
780
781
  }
781
782
 
782
- 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) {
783
787
  const safeResult = redactValue(result);
784
788
  if (typeof safeResult.content === "string") {
785
- safeResult.contentPreview = safeResult.content.slice(0, 600);
786
- safeResult.contentBytes = Buffer.byteLength(safeResult.content, "utf8");
787
- 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
+ }
788
798
  }
789
799
  return safeResult;
790
800
  }
@@ -884,7 +894,7 @@ async function captureSyntheticSnapshot(store, step, config) {
884
894
  : `Shell tool available in: ${config.commandCwd} on ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
885
895
  : "Shell tool disabled.",
886
896
  config.allowShellTool
887
- ? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist."
897
+ ? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist. For one-shot tmux commands, redirect output and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
888
898
  : "",
889
899
  config.allowFileTools
890
900
  ? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches. For new standalone generated content, pick a descriptive non-conflicting filename and avoid overwriting unless explicitly requested.`
@@ -75,6 +75,7 @@ export function formatBehaviorContractForPrompt({ mode = "runtime" } = {}) {
75
75
  "Keep artifacts durable and discoverable with descriptive non-conflicting names; never overwrite unless the user clearly asked.",
76
76
  "When reporting shell, language, runtime, build, or test results, name the actual environment used (host vs Docker, relevant interpreter/tool path/version when it matters). Do not claim compatibility across untested runtimes, hosts, containers, or language versions; state the caveat or run an explicit check.",
77
77
  "Do not self-invoke AgInTiFlow with npx/npm exec or nested aginti commands from inside the agent shell; it can resolve stale project packages or create recursive sessions. Use current runtime evidence, project/session files, or ask for a host-side diagnostic instead.",
78
+ "For tmux one-shot jobs, do not claim stdout, stderr, or exit status after the session disappears unless that output was captured or redirected to a durable workspace log. Prefer commands that write `...; echo EXIT:$? > logs/...` or keep the pane open long enough for `tmux_capture_pane`; if capture fails, report tmux evidence unavailable and rely only on separately verified evidence.",
78
79
  ].join(" ");
79
80
  }
80
81
 
@@ -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) {
@@ -769,7 +769,7 @@ export async function requestNextStep(client, config, messages) {
769
769
  function: {
770
770
  name: "tmux_capture_pane",
771
771
  description:
772
- "Capture recent text from a durable host tmux pane by target such as session:0.0. Use this to monitor progress or inspect a long-running job without interrupting it.",
772
+ "Capture recent text from a durable host tmux pane by target such as session:0.0. Use this to monitor progress or inspect a long-running job without interrupting it. If capture fails because the session ended, do not infer stdout/stderr/exit status; use a durable workspace log or rerun with output redirected to a file.",
773
773
  parameters: {
774
774
  type: "object",
775
775
  properties: {
@@ -812,7 +812,7 @@ export async function requestNextStep(client, config, messages) {
812
812
  function: {
813
813
  name: "tmux_start_session",
814
814
  description:
815
- "Start a detached durable host tmux session rooted inside the workspace, optionally with a startup command. Use for long-running local jobs that should be monitored with tmux_capture_pane instead of blocking the agent. This is the correct tmux path in Docker mode because run_command containers are ephemeral.",
815
+ "Start a detached durable host tmux session rooted inside the workspace, optionally with a startup command. Use for long-running local jobs that should be monitored with tmux_capture_pane instead of blocking the agent. For one-shot commands, redirect output and exit status to a durable workspace log or keep the shell open so capture can verify; do not claim results from an auto-terminated session. This is the correct tmux path in Docker mode because run_command containers are ephemeral.",
816
816
  parameters: {
817
817
  type: "object",
818
818
  properties: {
@@ -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: {