Reproduction sandbox: canonical (/private/var) paths so reproduce.sh can execute
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 changed file +16 −1
modified
services/research-runner/src/runner.ts
+16 −1
@@ -479,7 +479,22 @@ export class ResearchRunner { | |||||
| 479 | await mkdir(join(reproDir, "results/tables"), { recursive: true }); | 479 | await mkdir(join(reproDir, "results/tables"), { recursive: true }); | ||
| 480 | await mkdir(join(reproDir, "results/figures"), { recursive: true }); | 480 | await mkdir(join(reproDir, "results/figures"), { recursive: true }); | ||
| 481 | await mkdir(join(reproDir, "home"), { recursive: true }); | 481 | await mkdir(join(reproDir, "home"), { recursive: true }); | ||
| 482 | − const profile = `(version 1)\n(allow default)\n(deny file-write*)\n(allow file-write* (subpath "${reproDir}") (subpath "/private/tmp") (subpath "/tmp") (subpath "/private/var/folders") (literal "/dev/null") (regex #"^/dev/tty") (subpath "${this.host.tectonicCache ?? join(this.ws, ".cache/tectonic")}"))\n(deny file-read* (subpath "/Users") (subpath "/etc/autoscholar") (subpath "/var/autoscholar/runs") (subpath "/private/var/autoscholar/runs"))\n(allow file-read* (subpath "${reproDir}") (subpath "${this.ws}/.cache"))\n`; | 482 | + // Seatbelt matches canonical paths (/private/var/...): resolve every path and allow both spellings | ||
| 483 | + const real = async (p: string) => realpath(p).catch(() => p); | ||||
| 484 | + const reproReal = await real(reproDir); | ||||
| 485 | + const wsReal = await real(this.ws); | ||||
| 486 | + const cacheDir = this.host.tectonicCache ?? join(this.ws, ".cache/tectonic"); | ||||
| 487 | + const cacheReal = await real(cacheDir); | ||||
| 488 | + const both = (p: string, q: string) => [...new Set([p, q])].map((x) => `(subpath "${x}")`).join(" "); | ||||
| 489 | + const profile = [ | ||||
| 490 | + "(version 1)", | ||||
| 491 | + "(allow default)", | ||||
| 492 | + "(deny file-write*)", | ||||
| 493 | + `(allow file-write* ${both(reproDir, reproReal)} (subpath "/private/tmp") (subpath "/tmp") (subpath "/private/var/folders") (literal "/dev/null") (regex #"^/dev/tty") ${both(cacheDir, cacheReal)} ${both(join(this.ws, ".cache"), join(wsReal, ".cache"))})`, | ||||
| 494 | + `(deny file-read* (subpath "/Users") (subpath "/etc/autoscholar") (subpath "/private/etc/autoscholar") (subpath "/var/autoscholar/runs") (subpath "/private/var/autoscholar/runs"))`, | ||||
| 495 | + `(allow file-read* ${both(reproDir, reproReal)} ${both(join(this.ws, ".cache"), join(wsReal, ".cache"))} ${both(cacheDir, cacheReal)})`, | ||||
| 496 | + "", | ||||
| 497 | + ].join("\n"); | ||||
| 483 | const profilePath = join(this.ws, "_repro/repro.sb"); | 498 | const profilePath = join(this.ws, "_repro/repro.sb"); | ||
| 484 | await writeFile(profilePath, profile); | 499 | await writeFile(profilePath, profile); | ||
| 485 | const t0 = Date.now(); | 500 | const t0 = Date.now(); | ||
| 486 | 501 | ||||