@lazyingart/agintiflow 0.20.73 → 0.20.74

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.73",
3
+ "version": "0.20.74",
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",
@@ -530,6 +530,54 @@ try {
530
530
  });
531
531
  assert(envRun.events.some((event) => event.type === "tool.blocked"), ".env guardrail did not emit tool.blocked");
532
532
 
533
+ const secretContentResult = await executeWorkspaceTool(
534
+ "write_file",
535
+ {
536
+ path: "notes/secret-leak-report.md",
537
+ content: "Content attempted: DEMO_SECRET_TOKEN=aginti_fake_do_not_use\n",
538
+ mode: "create",
539
+ },
540
+ {
541
+ commandCwd: workspace,
542
+ allowFileTools: true,
543
+ }
544
+ );
545
+ assert(secretContentResult.blocked && secretContentResult.category === "workspace-content", "write_file secret-like content was not blocked");
546
+ await fs
547
+ .access(path.join(workspace, "notes/secret-leak-report.md"))
548
+ .then(() => {
549
+ throw new Error("secret-like report content was written despite content guardrails");
550
+ })
551
+ .catch((error) => {
552
+ if (error.code !== "ENOENT") throw error;
553
+ });
554
+
555
+ const redactedContentResult = await executeWorkspaceTool(
556
+ "write_file",
557
+ {
558
+ path: "notes/redacted-report.md",
559
+ content: "Content attempted: DEMO_SECRET_TOKEN=[REDACTED]\n",
560
+ mode: "create",
561
+ },
562
+ {
563
+ commandCwd: workspace,
564
+ allowFileTools: true,
565
+ }
566
+ );
567
+ assert(redactedContentResult.ok, "write_file should allow already-redacted secret placeholders");
568
+
569
+ const secretPatchResult = await executeWorkspaceTool(
570
+ "apply_patch",
571
+ {
572
+ patch: ["*** Begin Patch", "*** Add File: notes/secret-patch.md", "+DEMO_SECRET_TOKEN=aginti_fake_do_not_use", "*** End Patch"].join("\n"),
573
+ },
574
+ {
575
+ commandCwd: workspace,
576
+ allowFileTools: true,
577
+ }
578
+ );
579
+ assert(secretPatchResult.blocked && secretPatchResult.category === "workspace-content", "apply_patch secret-like additions were not blocked");
580
+
533
581
  const outsideRun = await runMock("Create file: ../outside-workspace.txt with blocked content.", "coding-block-outside");
534
582
  await fs
535
583
  .access(path.join(tempRoot, "outside-workspace.txt"))
@@ -576,6 +624,9 @@ try {
576
624
  "patch_guardrail",
577
625
  "patch_move_no_overwrite",
578
626
  "block_env",
627
+ "block_secret_write_content",
628
+ "allow_redacted_write_content",
629
+ "block_secret_patch_content",
579
630
  "block_outside",
580
631
  ],
581
632
  },
@@ -69,6 +69,7 @@ export function formatBehaviorContractForPrompt({ mode = "runtime" } = {}) {
69
69
  "Make surgical edits: no drive-by refactors, unrelated formatting churn, or deletion of code you did not need to touch.",
70
70
  "Define or infer concrete success criteria for non-trivial work, then run focused checks or state why checks are unavailable.",
71
71
  "Respect the permission contract: if a tool is blocked or returns permissionAdvice, stop and present the exact suggestedCommand/approval path instead of retrying variants or inventing CLI flags.",
72
+ "Protect secrets aggressively: never repeat token/key/password/secret values from prompts, files, tool output, plans, final answers, reports, diffs, or artifacts. Redact the value as [REDACTED] and use dedicated key storage such as `aginti keys set` when credentials are needed.",
72
73
  "Keep artifacts durable and discoverable with descriptive non-conflicting names; never overwrite unless the user clearly asked.",
73
74
  "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.",
74
75
  "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.",
package/src/redaction.js CHANGED
@@ -19,6 +19,11 @@ export function redactSensitiveText(value) {
19
19
  return text;
20
20
  }
21
21
 
22
+ export function hasSensitiveText(value) {
23
+ const text = String(value ?? "");
24
+ return redactSensitiveText(text) !== text;
25
+ }
26
+
22
27
  export function redactValue(value) {
23
28
  if (typeof value === "string") return redactSensitiveText(value);
24
29
  if (Array.isArray(value)) return value.map((item) => redactValue(item));
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { redactSensitiveText } from "./redaction.js";
4
+ import { hasSensitiveText, redactSensitiveText } from "./redaction.js";
5
5
 
6
6
  export const WORKSPACE_TOOL_NAMES = ["inspect_project", "list_files", "read_file", "search_files", "write_file", "apply_patch"];
7
7
  export const WORKSPACE_WRITE_TOOL_NAMES = ["write_file", "apply_patch"];
@@ -188,6 +188,37 @@ function pathPolicy(toolName, relativePath) {
188
188
  return { allowed: true };
189
189
  }
190
190
 
191
+ function secretContentPolicy(content) {
192
+ if (!hasSensitiveText(content)) return { allowed: true };
193
+ return {
194
+ allowed: false,
195
+ reason:
196
+ "Write content appears to contain token-like secret text. Redact secret values as [REDACTED] or use dedicated key storage such as `aginti keys set`; do not write credentials into workspace files.",
197
+ category: "workspace-content",
198
+ };
199
+ }
200
+
201
+ function writeContentPolicy(toolName, args, operations = null) {
202
+ if (toolName === "write_file") return secretContentPolicy(args.content || "");
203
+ if (toolName !== "apply_patch") return { allowed: true };
204
+
205
+ if (typeof args.patch === "string" && args.patch.trim()) {
206
+ for (const operation of operations || []) {
207
+ if (operation.type === "add") {
208
+ const policy = secretContentPolicy(operation.content || "");
209
+ if (!policy.allowed) return policy;
210
+ }
211
+ for (const hunk of operation.hunks || []) {
212
+ const policy = secretContentPolicy(hunk.replace || "");
213
+ if (!policy.allowed) return policy;
214
+ }
215
+ }
216
+ return { allowed: true };
217
+ }
218
+
219
+ return secretContentPolicy(args.replace ?? "");
220
+ }
221
+
191
222
  export function checkWorkspaceToolUse(toolName, args, config) {
192
223
  if (!WORKSPACE_TOOL_NAMES.includes(toolName)) return { allowed: true };
193
224
  if (!config.allowFileTools) {
@@ -196,17 +227,26 @@ export function checkWorkspaceToolUse(toolName, args, config) {
196
227
 
197
228
  try {
198
229
  if (toolName === "apply_patch" && typeof args.patch === "string" && args.patch.trim()) {
199
- for (const operation of parsePatchDocument(args.patch)) {
230
+ const operations = parsePatchDocument(args.patch);
231
+ for (const operation of operations) {
200
232
  for (const candidate of [operation.path, operation.newPath].filter(Boolean)) {
201
233
  const target = resolveWorkspacePath(config, candidate);
202
234
  const policy = pathPolicy(toolName, target.relativePath);
203
235
  if (!policy.allowed) return policy;
204
236
  }
205
237
  }
238
+ const contentPolicy = writeContentPolicy(toolName, args, operations);
239
+ if (!contentPolicy.allowed) return contentPolicy;
206
240
  return { allowed: true };
207
241
  }
208
242
  const target = resolveWorkspacePath(config, args.path || ".");
209
- return pathPolicy(toolName, target.relativePath);
243
+ const policy = pathPolicy(toolName, target.relativePath);
244
+ if (!policy.allowed) return policy;
245
+ if (WORKSPACE_WRITE_TOOL_NAMES.includes(toolName)) {
246
+ const contentPolicy = writeContentPolicy(toolName, args);
247
+ if (!contentPolicy.allowed) return contentPolicy;
248
+ }
249
+ return { allowed: true };
210
250
  } catch (error) {
211
251
  return {
212
252
  allowed: false,
@@ -591,6 +631,8 @@ async function writeChange(target, nextContent, action, details = {}) {
591
631
  if (Buffer.byteLength(content, "utf8") > MAX_WRITE_BYTES) {
592
632
  throw new Error(`Write is too large for safe workspace tools: ${target.relativePath}`);
593
633
  }
634
+ const contentPolicy = secretContentPolicy(content);
635
+ if (!contentPolicy.allowed) throw new Error(contentPolicy.reason);
594
636
 
595
637
  await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
596
638
  let beforeText = "";