@foldrun/core 1.4.0 → 1.4.2

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.
@@ -36,7 +36,9 @@ import { repoDir } from "./gitrepo.js";
36
36
  import { entriesNoFollow } from "./paths.js";
37
37
  import { filterIgnored, findingFingerprint, ruleForWhere, scanFiles } from "./scan.js";
38
38
  /** Directories a deploy never reads out of a source tree. */
39
- const NOT_SOURCE = new Set([".git", "node_modules", "runs", "outputs", ".foldrun", ".results"]);
39
+ // `.cache` is what a run's tools leave in an agent's folder (HOME is that
40
+ // folder): logs and font caches, never authored.
41
+ const NOT_SOURCE = new Set([".git", "node_modules", "runs", "outputs", ".foldrun", ".results", ".cache"]);
40
42
  /** Files that are never part of a workspace, whatever directory they sit in. */
41
43
  const NOT_SOURCE_FILE = new Set([".DS_Store", "secrets.json"]);
42
44
  const IN_WORKSPACE_DIR = new RegExp(`^(${WORKSPACE_DIRS.join("|")})/`);
@@ -59,6 +59,10 @@ const DENY_BACK_GIT_RE = /(^|\/)\.git(\/|$)/;
59
59
  const DENY_BACK_SECRET_FILES = /(^|\/)\.secret-files\//;
60
60
  // And per-agent knowledge, at any depth under agents/<name>/.
61
61
  const DENY_BACK_RE = /^agents\/[^/]+\/knowledge\//;
62
+ // A run's caches are not workspace content either: HOME is the agent's own
63
+ // folder, so a tool that ignores XDG_CACHE_HOME (set by the driver) still
64
+ // writes ~/.cache or ~/.npm there. Never copied back.
65
+ const DENY_BACK_CACHE = /^agents\/[^/]+\/\.(?:cache|npm)(?:\/|$)/;
62
66
  // A dependency tree is not workspace content. Nothing authored lives in
63
67
  // node_modules, it is enormous, and a runtime linked into a tool's directory
64
68
  // so ESM can resolve it would otherwise be copied back file by file.
@@ -80,6 +84,8 @@ export function allowedBack(rel) {
80
84
  return false;
81
85
  if (DENY_BACK_NODE_MODULES.test(norm))
82
86
  return false;
87
+ if (DENY_BACK_CACHE.test(norm))
88
+ return false;
83
89
  return true;
84
90
  }
85
91
  /**
@@ -433,6 +439,11 @@ try {
433
439
  // @file secrets crossed as content; materialise them to 0600 paths inside
434
440
  // the container (a host path from the other side would not exist here).
435
441
  const env = materializeFileSecrets(agentDir, { ...process.env, HOME: agentDir }).env;
442
+ // Caches go to the sandbox user's own home, not to HOME: the Claude CLI's
443
+ // logs, fontconfig, Mesa and gstreamer all follow XDG_CACHE_HOME, and under
444
+ // agents/<name>/.cache they were copied back after every step — 3,855 files
445
+ // across 179 agent folders on the box by 17 Oct 2026, read by nothing.
446
+ env.XDG_CACHE_HOME = "/home/agent/.cache";
436
447
 
437
448
  // The declared runtime, built in here — pip and npm have the network, which
438
449
  // the host side may not. It lands in .foldrun/runner/.runtimes, which the
package/dist/src/scan.js CHANGED
@@ -646,6 +646,13 @@ function ignored(ig, rule, line) {
646
646
  /** Installed and built code: someone else's files, and a crypto library's
647
647
  * constants are not this workspace's keys. */
648
648
  const VENDORED = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__|vendor|dist|build|\.next)\//;
649
+ /** What a run leaves in an agent's folder — the Claude CLI's `.cache/` logs,
650
+ * package caches — and copies back with it. Nobody wrote these, so they are
651
+ * not checked as prompts or code; they are still searched for credentials,
652
+ * since a key in a log here is a key in the workspace (seen live on
653
+ * 17 Oct 2026: zero-width characters in a CLI MCP log, reported as hidden
654
+ * text). */
655
+ const RUNTIME_WRITTEN = /(?:^|\/)(?:\.cache|\.npm|\.local|\.pnpm-store|\.yarn|\.foldrun)\//;
649
656
  /** Text files only — a PNG read as UTF-8 is noise to every rule here. */
650
657
  function isText(f) {
651
658
  return !VENDORED.test(f.path) && !f.content.slice(0, 8000).includes("\u0000") && !/\.(?:png|jpe?g|gif|webp|ico|pdf|zip|gz|woff2?|ttf|mp[34]|mov|wasm)$/i.test(f.path);
@@ -679,6 +686,8 @@ export function scanFiles(files, options = {}) {
679
686
  const lines = f.content.split(/\r?\n/);
680
687
  ignoresByFile.set(f.path, readIgnores(f, lines, push));
681
688
  scanSecrets(f, lines, push);
689
+ if (RUNTIME_WRITTEN.test(f.path))
690
+ continue;
682
691
  scanContent(f, lines, push);
683
692
  scanSupplyChain(f, lines, push);
684
693
  const schema = schemaKindOf(f.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldrun/core",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Agents are just folders. The format, the runner, the checks.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
package/src/deploy.ts CHANGED
@@ -49,7 +49,9 @@ import { entriesNoFollow } from "./paths.ts";
49
49
  import { filterIgnored, findingFingerprint, ruleForWhere, scanFiles, type ScanFinding, type ScanOptions } from "./scan.ts";
50
50
 
51
51
  /** Directories a deploy never reads out of a source tree. */
52
- const NOT_SOURCE = new Set([".git", "node_modules", "runs", "outputs", ".foldrun", ".results"]);
52
+ // `.cache` is what a run's tools leave in an agent's folder (HOME is that
53
+ // folder): logs and font caches, never authored.
54
+ const NOT_SOURCE = new Set([".git", "node_modules", "runs", "outputs", ".foldrun", ".results", ".cache"]);
53
55
 
54
56
  /** Files that are never part of a workspace, whatever directory they sit in. */
55
57
  const NOT_SOURCE_FILE = new Set([".DS_Store", "secrets.json"]);
@@ -192,6 +192,10 @@ const DENY_BACK_GIT_RE = /(^|\/)\.git(\/|$)/;
192
192
  const DENY_BACK_SECRET_FILES = /(^|\/)\.secret-files\//;
193
193
  // And per-agent knowledge, at any depth under agents/<name>/.
194
194
  const DENY_BACK_RE = /^agents\/[^/]+\/knowledge\//;
195
+ // A run's caches are not workspace content either: HOME is the agent's own
196
+ // folder, so a tool that ignores XDG_CACHE_HOME (set by the driver) still
197
+ // writes ~/.cache or ~/.npm there. Never copied back.
198
+ const DENY_BACK_CACHE = /^agents\/[^/]+\/\.(?:cache|npm)(?:\/|$)/;
195
199
  // A dependency tree is not workspace content. Nothing authored lives in
196
200
  // node_modules, it is enormous, and a runtime linked into a tool's directory
197
201
  // so ESM can resolve it would otherwise be copied back file by file.
@@ -207,6 +211,7 @@ export function allowedBack(rel: string): boolean {
207
211
  if (DENY_BACK_GIT_RE.test(norm)) return false;
208
212
  if (DENY_BACK_SECRET_FILES.test(norm)) return false;
209
213
  if (DENY_BACK_NODE_MODULES.test(norm)) return false;
214
+ if (DENY_BACK_CACHE.test(norm)) return false;
210
215
  return true;
211
216
  }
212
217
 
@@ -564,6 +569,11 @@ try {
564
569
  // @file secrets crossed as content; materialise them to 0600 paths inside
565
570
  // the container (a host path from the other side would not exist here).
566
571
  const env = materializeFileSecrets(agentDir, { ...process.env, HOME: agentDir }).env;
572
+ // Caches go to the sandbox user's own home, not to HOME: the Claude CLI's
573
+ // logs, fontconfig, Mesa and gstreamer all follow XDG_CACHE_HOME, and under
574
+ // agents/<name>/.cache they were copied back after every step — 3,855 files
575
+ // across 179 agent folders on the box by 17 Oct 2026, read by nothing.
576
+ env.XDG_CACHE_HOME = "/home/agent/.cache";
567
577
 
568
578
  // The declared runtime, built in here — pip and npm have the network, which
569
579
  // the host side may not. It lands in .foldrun/runner/.runtimes, which the
package/src/scan.ts CHANGED
@@ -729,6 +729,14 @@ type Push = (rule: string, file: string, line: number | undefined, text: string,
729
729
  * constants are not this workspace's keys. */
730
730
  const VENDORED = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__|vendor|dist|build|\.next)\//;
731
731
 
732
+ /** What a run leaves in an agent's folder — the Claude CLI's `.cache/` logs,
733
+ * package caches — and copies back with it. Nobody wrote these, so they are
734
+ * not checked as prompts or code; they are still searched for credentials,
735
+ * since a key in a log here is a key in the workspace (seen live on
736
+ * 17 Oct 2026: zero-width characters in a CLI MCP log, reported as hidden
737
+ * text). */
738
+ const RUNTIME_WRITTEN = /(?:^|\/)(?:\.cache|\.npm|\.local|\.pnpm-store|\.yarn|\.foldrun)\//;
739
+
732
740
  /** Text files only — a PNG read as UTF-8 is noise to every rule here. */
733
741
  function isText(f: ScanFile): boolean {
734
742
  return !VENDORED.test(f.path) && !f.content.slice(0, 8000).includes("\u0000") && !/\.(?:png|jpe?g|gif|webp|ico|pdf|zip|gz|woff2?|ttf|mp[34]|mov|wasm)$/i.test(f.path);
@@ -763,6 +771,7 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
763
771
  const lines = f.content.split(/\r?\n/);
764
772
  ignoresByFile.set(f.path, readIgnores(f, lines, push));
765
773
  scanSecrets(f, lines, push);
774
+ if (RUNTIME_WRITTEN.test(f.path)) continue;
766
775
  scanContent(f, lines, push);
767
776
  scanSupplyChain(f, lines, push);
768
777