





















@@ -1,3 +1,5 @@
1+import { constants as fsConstants } from "node:fs";
2+import fs from "node:fs/promises";
13import path from "node:path";
24import type { AgentToolResult } from "@earendil-works/pi-agent-core";
35import { buildCommandPayloadCandidates } from "../infra/command-analysis/risks.js";
@@ -113,6 +115,12 @@ const SKIPPABLE_SCRIPT_PREFLIGHT_FS_ERROR_CODES = new Set([
113115"ENOTDIR",
114116"EPERM",
115117]);
118+const SCRIPT_PREFLIGHT_MAX_BYTES = 512 * 1024;
119+const FS_CONSTANTS_WITH_OPTIONAL_NONBLOCK = fsConstants as typeof fsConstants & {
120+O_NONBLOCK?: number;
121+};
122+const SCRIPT_PREFLIGHT_OPEN_FLAGS =
123+fsConstants.O_RDONLY | (FS_CONSTANTS_WITH_OPTIONAL_NONBLOCK.O_NONBLOCK ?? 0);
116124117125function getNodeErrorCode(error: unknown): string | undefined {
118126if (typeof error !== "object" || error === null || !("code" in error)) {
@@ -149,9 +157,46 @@ function resolvePreflightRelativePath(params: { rootDir: string; absPath: string
149157if (/^\.\.(?:[\\/]|$)/u.test(relative) || path.isAbsolute(relative)) {
150158return null;
151159}
152-// Preserve literal "~" path segments under the workdir. Root reads
153-// expand home prefixes for relative paths, so normalize `~/...` to `./~/...`.
154-return /^~(?:$|[\\/])/u.test(relative) ? `.${path.sep}${relative}` : relative;
160+return relative;
161+}
162+163+function hasLeadingTildePathSegment(relativePath: string): boolean {
164+return /^~(?:$|[\\/])/u.test(relativePath);
165+}
166+167+async function readLiteralTildePreflightScript(params: {
168+absPath: string;
169+fsSafe: FsSafeModule;
170+workspaceRoot: Awaited<ReturnType<FsSafeModule["root"]>>;
171+}): Promise<string> {
172+let handle: fs.FileHandle | undefined;
173+try {
174+handle = await fs.open(params.absPath, SCRIPT_PREFLIGHT_OPEN_FLAGS);
175+const stat = await handle.stat();
176+if (!stat.isFile()) {
177+throw new params.fsSafe.FsSafeError("not-file", "not a file");
178+}
179+if (stat.size > SCRIPT_PREFLIGHT_MAX_BYTES) {
180+throw new params.fsSafe.FsSafeError(
181+"too-large",
182+`file exceeds limit of ${SCRIPT_PREFLIGHT_MAX_BYTES} bytes (got ${stat.size})`,
183+);
184+}
185+const realPath = await params.fsSafe.resolveOpenedFileRealPathForHandle(handle, params.absPath);
186+if (!params.fsSafe.isPathInside(params.workspaceRoot.rootReal, realPath)) {
187+throw new params.fsSafe.FsSafeError("outside-workspace", "file is outside workspace root");
188+}
189+const buffer = await handle.readFile();
190+if (buffer.byteLength > SCRIPT_PREFLIGHT_MAX_BYTES) {
191+throw new params.fsSafe.FsSafeError(
192+"too-large",
193+`file exceeds limit of ${SCRIPT_PREFLIGHT_MAX_BYTES} bytes (got ${buffer.byteLength})`,
194+);
195+}
196+return buffer.toString("utf-8");
197+} finally {
198+await handle?.close().catch(() => undefined);
199+}
155200}
156201157202function isShellEnvAssignmentToken(token: string): boolean {
@@ -967,7 +1012,8 @@ async function validateScriptFileForShellBleed(params: {
9671012return;
9681013}
9691014970-const { FsSafeError, root: fsRoot } = await loadFsSafeModule();
1015+const fsSafe = await loadFsSafeModule();
1016+const { FsSafeError, root: fsRoot } = fsSafe;
9711017const workspaceRoot = await fsRoot(params.workdir);
9721018for (const relOrAbsPath of target.relOrAbsPaths) {
9731019const absPath = path.isAbsolute(relOrAbsPath)
@@ -987,12 +1033,19 @@ async function validateScriptFileForShellBleed(params: {
9871033// Use non-blocking open to avoid stalls if a path is swapped to a FIFO.
9881034let content: string;
9891035try {
990-const safeRead = await workspaceRoot.read(relativePath, {
991-nonBlockingRead: true,
992-symlinks: "follow-within-root",
993-maxBytes: 512 * 1024,
994-});
995-content = safeRead.buffer.toString("utf-8");
1036+content = hasLeadingTildePathSegment(relativePath)
1037+ ? await readLiteralTildePreflightScript({
1038+ absPath,
1039+ fsSafe,
1040+ workspaceRoot,
1041+})
1042+ : (
1043+await workspaceRoot.read(relativePath, {
1044+nonBlockingRead: true,
1045+symlinks: "follow-within-root",
1046+maxBytes: SCRIPT_PREFLIGHT_MAX_BYTES,
1047+})
1048+).buffer.toString("utf-8");
9961049} catch (error) {
9971050if (shouldSkipScriptPreflightPathError(error, FsSafeError)) {
9981051// Preflight validation is best-effort: skip path/read failures and
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。