@sema-agent/core
Version:
Stateless, task-oriented AI agent core
871 lines (870 loc) • 42.5 kB
JavaScript
import { createHash } from "node:crypto";
import { readDenySentence } from "./read-deny.js";
import { homeViewsOf, homeViewUnreadSentence } from "./read-deny-home.js";
import { skipsRootsContainment } from "./containment-face.js";
import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
import { fileArgPath, pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
import { readPath } from "../../core/effective-path-target.js";
import { isUncSpelling } from "../../core/unc-spelling.js";
import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
import { thrownText } from "../../core/thrown-value.js";
export function writeTargetPath(call, pt = protectivePathTargetOf(call)) {
if (pt !== undefined)
return pathTargetValue(call.args, pt);
const np = call.args?.notebook_path;
const notebook = typeof np === "string" && np.length > 0 ? np : undefined;
return fileArgPath(call.args) ?? notebook;
}
export function sha256(content) {
return createHash("sha256").update(content, "utf8").digest("hex");
}
const SEP = "/";
export function relativeInEveryFamily(p) {
return !(p.startsWith("/") || p.startsWith("\\") || /^[A-Za-z]:/.test(p));
}
function nativeSepOf(p) {
return isWinFormPath(p) && p.includes("\\") ? "\\" : "/";
}
function lastSepIndex(p) {
return isWinFormPath(p) ? Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) : p.lastIndexOf("/");
}
const BLOCKED_DEVICE_PATHS = new Set([
"/dev/zero", "/dev/random", "/dev/urandom", "/dev/full",
"/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/console",
"/dev/fd/0", "/dev/fd/1", "/dev/fd/2",
]);
function isProcStdioFd(key) {
return key.startsWith("/proc/") && (key.endsWith("/fd/0") || key.endsWith("/fd/1") || key.endsWith("/fd/2"));
}
export function namesStandardInput(path) {
const k = path.startsWith("/") ? normalizeAbsPathLexically(path) : "";
return k === "/dev/stdin" || k === "/dev/fd/0" || (k.startsWith("/proc/") && k.endsWith("/fd/0"));
}
const PROC_SENSITIVE_SUFFIXES = ["/environ", "/cmdline", "/auxv", "/maps", "/mem", "/stat"];
function isProcSensitiveFile(key) {
return key.startsWith("/proc/") && PROC_SENSITIVE_SUFFIXES.some((suf) => key.endsWith(suf));
}
const WIN_RESERVED_RE = /^(CON|PRN|AUX|NUL|COM[0-9¹²³]|LPT[0-9¹²³]|CONIN\$|CONOUT\$)(\.[^\\/]*)?$/i;
export function isWinFormPath(p) {
return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
}
export function expandHomeTilde(p, home) {
if (p === "~")
return home;
if (!p.startsWith("~"))
return p;
const next = p[1];
if (next !== "/" && next !== "\\")
return p;
return home + "/" + p.slice(2);
}
function isWinReservedDeviceKey(key) {
if (!isWinFormPath(key))
return false;
const t = key.replace(/[\\/]+$/, "");
const base = t.slice(Math.max(t.lastIndexOf("/"), t.lastIndexOf("\\")) + 1);
return WIN_RESERVED_RE.test(base);
}
export function isBlockedDevicePath(key) {
return BLOCKED_DEVICE_PATHS.has(key) || isProcStdioFd(key) || isProcSensitiveFile(key) || isWinReservedDeviceKey(key);
}
export function normalizeAbsPathLexically(p) {
return foldSegmentsLexically(p, true);
}
export function foldRedundantPathSegments(p) {
return foldSegmentsLexically(p, false);
}
function foldSegmentsLexically(p, foldParents) {
p = stripWin32ExtendedPrefix(p);
const isUnc = p.startsWith("\\\\");
const drive = /^[A-Za-z]:/.exec(p)?.[0] ?? "";
const winForm = isUnc || drive !== "" || (!p.startsWith("/") && p.includes("\\"));
const sep = winForm && p.includes("\\") ? "\\" : "/";
const splitter = winForm ? /[\\/]+/ : /\/+/;
let prefix = drive;
let rest = p.slice(drive.length + (isUnc ? 2 : 0));
let abs = rest.startsWith("/") || (winForm && rest.startsWith("\\"));
if (isUnc) {
const segs = rest.split(splitter).filter((seg) => seg !== "");
const host = segs.shift() ?? "";
const share = segs.shift();
prefix = sep + sep + host + (share === undefined ? "" : sep + share);
rest = segs.join(sep);
abs = true;
}
const outSegs = [];
for (const seg of rest.split(splitter)) {
if (seg === "" || seg === ".")
continue;
if (seg === "..") {
if (!foldParents) {
outSegs.push(seg);
continue;
}
if (outSegs.length > 0 && outSegs[outSegs.length - 1] !== "..")
outSegs.pop();
else if (!abs)
outSegs.push("..");
continue;
}
outSegs.push(seg);
}
if (outSegs.length === 0)
return abs ? (isUnc ? prefix : prefix + sep) : prefix === "" ? "." : prefix;
return prefix + (abs ? sep : "") + outSegs.join(sep);
}
const BINARY_EXTENSIONS = new Set([
".exe", ".dll", ".so", ".dylib", ".o", ".a", ".obj", ".lib", ".bin", ".class", ".pyc", ".pyo", ".wasm",
".app", ".msi", ".deb", ".rpm", ".node", ".rlib",
".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".jar", ".war", ".ear", ".lz4", ".zst", ".z",
".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".ico", ".heic", ".avif",
".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".aiff", ".opus",
".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv", ".flv", ".m4v", ".mpeg", ".mpg",
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp",
".woff", ".woff2", ".ttf", ".otf", ".eot",
".psd", ".ai", ".eps", ".sketch", ".fig", ".xd", ".blend", ".3ds", ".max", ".swf", ".fla",
".db", ".sqlite", ".sqlite3", ".mdb", ".idx", ".dat", ".data", ".lockb",
".pdb", ".ipa", ".apk", ".dmg", ".iso", ".img",
]);
export function hasBinaryExtension(path) {
const dot = path.lastIndexOf(".");
if (dot < 0)
return false;
return BINARY_EXTENSIONS.has(path.slice(dot).toLowerCase());
}
const IMAGE_MIME_BY_EXT = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
};
export function imageMimeForRead(path) {
const dot = path.lastIndexOf(".");
if (dot < 0)
return undefined;
return IMAGE_MIME_BY_EXT[path.slice(dot).toLowerCase()];
}
const asciiBytes = (s) => Array.from(s, (c) => c.charCodeAt(0));
export const BINARY_MAGIC_SIGNATURES = [
{ format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x03, 0x04] }] },
{ format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x05, 0x06] }] },
{ format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x07, 0x08] }] },
{ format: "gzip archive", segments: [{ offset: 0, bytes: [0x1f, 0x8b, 0x08] }] },
...[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39].map((level) => ({
format: "bzip2 archive",
segments: [{ offset: 0, bytes: [0x42, 0x5a, 0x68, level] }],
})),
{ format: "xz archive", segments: [{ offset: 0, bytes: [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00] }] },
{ format: "zstd archive", segments: [{ offset: 0, bytes: [0x28, 0xb5, 0x2f, 0xfd] }] },
{ format: "lz4 archive", segments: [{ offset: 0, bytes: [0x04, 0x22, 0x4d, 0x18] }] },
{ format: "compress archive", segments: [{ offset: 0, bytes: [0x1f, 0x9d] }] },
{ format: "7-Zip archive", segments: [{ offset: 0, bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c] }] },
{ format: "RAR archive", segments: [{ offset: 0, bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00] }] },
{ format: "RAR archive", segments: [{ offset: 0, bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00] }] },
{ format: "tar archive", segments: [{ offset: 257, bytes: asciiBytes("ustar") }] },
{ format: "ar archive (static library or Debian package)", segments: [{ offset: 0, bytes: asciiBytes("!<arch>\n") }] },
{ format: "RPM package", segments: [{ offset: 0, bytes: [0xed, 0xab, 0xee, 0xdb] }] },
{ format: "ISO 9660 disc image", segments: [{ offset: 32769, bytes: asciiBytes("CD001") }] },
{ format: "PNG image", segments: [{ offset: 0, bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }] },
{ format: "JPEG image", segments: [{ offset: 0, bytes: [0xff, 0xd8, 0xff] }] },
{ format: "GIF image", segments: [{ offset: 0, bytes: asciiBytes("GIF87a") }] },
{ format: "GIF image", segments: [{ offset: 0, bytes: asciiBytes("GIF89a") }] },
{ format: "TIFF image", segments: [{ offset: 0, bytes: [0x49, 0x49, 0x2a, 0x00] }] },
{ format: "TIFF image", segments: [{ offset: 0, bytes: [0x4d, 0x4d, 0x00, 0x2a] }] },
{ format: "BMP image", segments: [{ offset: 0, bytes: asciiBytes("BM") }, { offset: 6, bytes: [0x00, 0x00, 0x00, 0x00] }] },
{ format: "ICO image", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0x00] }] },
{ format: "Photoshop document", segments: [{ offset: 0, bytes: asciiBytes("8BPS") }] },
{ format: "WebP image", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("WEBP") }] },
{ format: "RIFF audio (WAV)", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("WAVE") }] },
{ format: "RIFF video (AVI)", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("AVI ") }] },
{ format: "AIFF audio", segments: [{ offset: 0, bytes: asciiBytes("FORM") }, { offset: 8, bytes: asciiBytes("AIFF") }] },
{ format: "Ogg container", segments: [{ offset: 0, bytes: asciiBytes("OggS") }, { offset: 4, bytes: [0x00] }] },
{ format: "FLAC audio", segments: [{ offset: 0, bytes: asciiBytes("fLaC") }] },
...[0x02, 0x03, 0x04].map((major) => ({
format: "MP3 audio (ID3 tag)",
segments: [{ offset: 0, bytes: [0x49, 0x44, 0x33, major] }],
})),
{ format: "ISO base media container (MP4/MOV/HEIC)", segments: [{ offset: 4, bytes: asciiBytes("ftyp") }] },
{ format: "Matroska/WebM container", segments: [{ offset: 0, bytes: [0x1a, 0x45, 0xdf, 0xa3] }] },
{ format: "ASF container (WMV/WMA)", segments: [{ offset: 0, bytes: [0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c] }] },
{ format: "FLV video", segments: [{ offset: 0, bytes: [0x46, 0x4c, 0x56, 0x01] }] },
{ format: "MPEG program stream", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0xba] }] },
{ format: "MPEG video stream", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0xb3] }] },
{ format: "ELF binary", segments: [{ offset: 0, bytes: [0x7f, 0x45, 0x4c, 0x46] }] },
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xfe, 0xed, 0xfa, 0xce] }] },
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xfe, 0xed, 0xfa, 0xcf] }] },
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xce, 0xfa, 0xed, 0xfe] }] },
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xcf, 0xfa, 0xed, 0xfe] }] },
{ format: "Java class file or Mach-O universal binary", segments: [{ offset: 0, bytes: [0xca, 0xfe, 0xba, 0xbe] }] },
{ format: "WebAssembly module", segments: [{ offset: 0, bytes: [0x00, 0x61, 0x73, 0x6d] }] },
{ format: "OLE compound document (Office 97-2003 or MSI)", segments: [{ offset: 0, bytes: [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1] }] },
{ format: "SQLite database", segments: [{ offset: 0, bytes: [...asciiBytes("SQLite format 3"), 0x00] }] },
{ format: "WOFF font", segments: [{ offset: 0, bytes: asciiBytes("wOFF") }] },
{ format: "WOFF2 font", segments: [{ offset: 0, bytes: asciiBytes("wOF2") }] },
{ format: "OpenType font", segments: [{ offset: 0, bytes: asciiBytes("OTTO") }] },
{ format: "TrueType font", segments: [{ offset: 0, bytes: [0x00, 0x01, 0x00, 0x00] }] },
{ format: "TrueType font collection", segments: [{ offset: 0, bytes: asciiBytes("ttcf") }] },
];
export function binaryMagicFormat(bytes) {
for (const signature of BINARY_MAGIC_SIGNATURES) {
let matched = true;
for (const segment of signature.segments) {
if (bytes.length < segment.offset + segment.bytes.length) {
matched = false;
break;
}
for (let i = 0; i < segment.bytes.length; i++) {
if (bytes[segment.offset + i] !== segment.bytes[i]) {
matched = false;
break;
}
}
if (!matched)
break;
}
if (matched)
return signature.format;
}
return undefined;
}
export function isBinaryContent(sample) {
if (sample.length === 0)
return false;
let nonPrintable = 0;
for (let i = 0; i < sample.length; i++) {
const c = sample.charCodeAt(i);
if (c === 0)
return true;
if (c >= 0xd800 && c <= 0xdbff) {
const next = i + 1 < sample.length ? sample.charCodeAt(i + 1) : -1;
if (next < 0xdc00 || next > 0xdfff)
return true;
i++;
continue;
}
if (c >= 0xdc00 && c <= 0xdfff)
return true;
if (c === 0xfffe || c === 0xffff)
return true;
if ((c < 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d && c !== 0x0c && c !== 0x0b) || c === 0xfffd) {
nonPrintable++;
}
}
return nonPrintable / sample.length > 0.1;
}
export function isUncPath(path) {
return path.startsWith("\\\\") || path.startsWith("//");
}
function isWin32DeviceNamespacePath(p) {
return p.startsWith("\\\\.\\") || p.startsWith("\\\\./");
}
function stripWin32ExtendedPrefix(p) {
if (!p.startsWith("\\\\?\\"))
return p;
const rest = p.slice(4);
const unc = /^UNC\\/i.exec(rest);
const body = unc !== null ? rest.slice(unc[0].length) : /^[A-Za-z]:\\/.test(rest) ? rest.slice(3) : undefined;
if (body === undefined)
return p;
if (body !== "") {
for (const seg of body.split("\\")) {
if (seg === "" || seg === "." || seg === "..")
return p;
if (seg.endsWith(".") || seg.endsWith(" ") || seg.includes("/"))
return p;
if (WIN_RESERVED_RE.test(seg))
return p;
}
}
return unc ? "\\\\" + rest.slice(unc[0].length) : rest;
}
export function hasWin32AliasedComponent(p) {
if (!isWinFormPath(p))
return false;
const parts = p.split(/[\\/]+/).filter((seg) => seg.length > 0);
const tail = p.startsWith("\\\\") || p.startsWith("//") ? parts.slice(2) : parts;
return tail.some((seg) => seg !== "." && seg !== ".." && /[. ]$/.test(seg));
}
export function pathFamilyOf(bases) {
const tree = bases.root ?? bases.cwd;
return tree === undefined ? undefined : isWinFormPath(tree) ? "win" : "posix";
}
export function familyFor(source, p) {
return source === "own" ? (isWinFormPath(p) ? "win" : "posix") : source;
}
export function isAbsoluteForFamily(family, p) {
return familyFor(family, p) === "win" ? /^[A-Za-z]:[\\/]/.test(p) || isUncSpelling(p) : p.startsWith("/");
}
export const WIN32_NAMESPACE_REFUSALS = Object.freeze(["device_namespace", "unmappable_namespace", "unc_on_posix_tree", "drive_relative"]);
export const WIN32_NAMESPACE_REFUSAL_TEXT = {
device_namespace: "is a Win32 device-namespace path (\\\\.\\ names a raw device object, not a file)",
unmappable_namespace: "is a Win32 extended-length namespace path with no DOS-path equivalent (only \\\\?\\<drive>: and \\\\?\\UNC\\ forms name files)",
unc_on_posix_tree: "is a UNC (\\\\host\\share) spelling, which names no file on a POSIX tree",
drive_relative: "is drive-relative (a drive letter with no separator after the colon), a location no tree can locate",
};
export function win32NamespaceScreen(spelled, family) {
if (family === "win" && /^[\\/]{2}[?.][\\/]/.test(spelled) && !/^\\\\[?.]\\/.test(spelled))
spelled = spelled.replace(/\//g, "\\");
if (isWin32DeviceNamespacePath(spelled))
return { ok: false, refuse: "device_namespace" };
const mapped = stripWin32ExtendedPrefix(spelled);
if (mapped.startsWith("\\\\?\\"))
return { ok: false, refuse: "unmappable_namespace" };
if (mapped.startsWith("\\\\") && family === "posix")
return { ok: false, refuse: "unc_on_posix_tree" };
if (family === "win" && /^[A-Za-z]:(?![\\/])/.test(mapped))
return { ok: false, refuse: "drive_relative" };
return { ok: true, spelled: mapped };
}
export function pathIdentityOf(spelled, family, anchor) {
if (typeof spelled !== "string")
return { ok: false, reason: "relative" };
if (family !== "win") {
if (!spelled.startsWith("/"))
return { ok: false, reason: "relative" };
return { ok: true, identity: mintIdentity("", spelled.split("/"), false) };
}
const screened = win32NamespaceScreen(spelled, "win");
if (!screened.ok)
return { ok: false, reason: screened.refuse };
let s = screened.spelled;
if (s.startsWith("/") && isAbsoluteForFamily("win", s.slice(1)))
s = s.slice(1);
if (isUncSpelling(s)) {
const segs = s.slice(2).split(/[\\/]+/).filter((x) => x !== "");
const host = segs.shift();
const share = segs.shift();
return { ok: true, identity: mintIdentity(`//${host}/${share}`, segs, true) };
}
const drive = /^([A-Za-z]):[\\/]/.exec(s) ?? /^[\\/]([A-Za-z])(?=[\\/]|$)/.exec(s);
if (drive !== null)
return { ok: true, identity: mintIdentity(`/${drive[1].toLowerCase()}`, s.slice(2).split(/[\\/]/), false) };
if (s.startsWith("/") || s.startsWith("\\")) {
const root = anchor === undefined ? undefined : pathIdentityOf(anchor, "win");
if (root === undefined || !root.ok)
return { ok: false, reason: "unanchored" };
return { ok: true, identity: mintIdentity(identityAnchorOf(root.identity), s.slice(1).split(/[\\/]/), false) };
}
return { ok: false, reason: "relative" };
}
function mintIdentity(anchor, segments, stripTail) {
const out = [];
for (const seg of segments) {
if (seg === "" || seg === ".")
continue;
if (seg === "..") {
if (out.length > 0)
out.pop();
continue;
}
const stripped = stripTail ? seg.slice(0, trailingDotSpaceStart(seg)) : seg;
out.push(stripped.length > 0 ? stripped : seg);
}
return (out.length === 0 ? anchor || "/" : `${anchor}/${out.join("/")}`);
}
function trailingDotSpaceStart(seg) {
let end = seg.length;
while (end > 0 && (seg[end - 1] === "." || seg[end - 1] === " "))
end -= 1;
return end;
}
function identityAnchorOf(identity) {
return identity.startsWith("//") ? identity.split("/").slice(0, 4).join("/") : identity.slice(0, 2);
}
export function isUncIdentity(identity) {
return identity.startsWith("//");
}
export function isPathIdentity(s, family) {
const r = pathIdentityOf(s, family);
return r.ok && r.identity === s;
}
export function isIdentityDir(identity, family) {
if (family !== "win")
return identity !== "/";
return identity.length > identityAnchorOf(identity).length;
}
export function spellingOfIdentity(identity, family) {
if (family !== "win")
return identity;
if (isUncIdentity(identity))
return identity.replace(/\//g, "\\");
return `${identity.slice(1, 2)}:${(identity.length > 2 ? identity.slice(2) : "/").replace(/\//g, "\\")}`;
}
export function carriesDotSegment(spelled, family) {
return (family === "win" ? /(^|[\\/])\.{1,2}([\\/]|$)/ : /(^|\/)\.{1,2}(\/|$)/).test(spelled);
}
export function clipSpelling(spelled) {
return spelled.length > 512 ? `${spelled.slice(0, 512)}…` : spelled;
}
export function isShellRootedSpellingUnmapped(family, spelled) {
return family === "win" && (spelled.startsWith("/") || spelled.startsWith("\\"));
}
export function nativeUncSpellingOf(family, spelled) {
return family === "win" && isUncSpelling(spelled) ? `\\\\${spelled.slice(2).replace(/\//g, "\\")}` : spelled;
}
export function foldTrailingSeparators(spelled, family) {
let end = spelled.length;
while (end > 0 && (spelled[end - 1] === "/" || (family === "win" && spelled[end - 1] === "\\")))
end -= 1;
if (end === spelled.length)
return spelled;
const stripped = spelled.slice(0, end);
const identity = pathIdentityOf(stripped, family);
if (!identity.ok || !isIdentityDir(identity.identity, family))
return spelled;
return stripped;
}
export function joinForFamily(family, base, rel) {
if (family === "posix")
return `${base.replace(/\/+$/, "")}/${rel}`;
if (rel.startsWith("/") || rel.startsWith("\\")) {
const root = /^[A-Za-z]:/.exec(base)?.[0] ?? /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/.exec(base)?.[0];
return root === undefined ? rel : root + rel;
}
return `${base.replace(/[\\/]+$/, "")}${base.includes("\\") ? "\\" : "/"}${rel}`;
}
export function anchorSpelling(family, base, spelled) {
return isAbsoluteForFamily(family, spelled) ? spelled : joinForFamily(family, base, spelled);
}
export function lexicalViewOf(spelled, base) {
return normalizeAbsPathLexically(anchorSpelling(pathFamilyOf({ root: base }), base, spelled));
}
function within(rootCanonical, p) {
if (!rootCanonical)
return false;
const family = pathFamilyOf({ root: rootCanonical });
const rootN = pathIdentityOf(rootCanonical, family);
const pN = pathIdentityOf(p, family);
if (!rootN.ok || !pN.ok)
return false;
if (pN.identity === rootN.identity)
return true;
return pN.identity.startsWith(rootN.identity.endsWith(SEP) ? rootN.identity : rootN.identity + SEP);
}
export async function resolveKey(env, rootCanonical, path, signal, baseCwd, additionalRoots, exactFileReadExemption, readDeny, containmentFace) {
const views = await readPath(env, path, baseCwd ?? rootCanonical, { root: rootCanonical, ...(baseCwd !== undefined ? { cwd: baseCwd } : {}) }, { intent: "resolve", views: readDeny !== undefined ? "chain" : "canonical" }, signal);
const canon = views.canonical ?? (await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootCanonical }), baseCwd));
if (!canon.ok)
return { ok: false, violation: { code: "invalid", message: canon.message } };
const key = canon.key;
const additionalRootsCanonical = typeof additionalRoots === "function" ? additionalRoots() : additionalRoots;
if (isBlockedDevicePath(key)) {
return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
}
if (readDeny !== undefined) {
const judged = { aliasResolved: canon.aliasResolved === true, hops: views.hops };
const lexical = views.lexical?.kind === "path" ? spellingOfIdentity(views.lexical.identity, views.lexical.family) : undefined;
let hit = readDeny.matchTarget(key, lexical, judged);
const write = readDeny.face === "write";
if (hit === null) {
const home = await homeViewsOf(env, readDeny, signal).of(key);
if (home.view !== undefined)
hit = readDeny.matchTarget(key, home.view, judged);
if (hit === null && home.unread !== undefined) {
return { ok: false, violation: { code: write ? "write_path_denied" : "read_path_denied", message: homeViewUnreadSentence(path, home.unread, write ? "write" : "read"), target: key } };
}
}
if (hit !== null) {
return {
ok: false,
violation: {
code: write ? "write_path_denied" : "read_path_denied",
message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}), ...(write ? { face: "write" } : {}) }),
target: key,
pattern: hit.pattern,
},
};
}
}
if (skipsRootsContainment(containmentFace)) {
if (isUncPath(key) && isWinFormPath(key) && !within(rootCanonical, key) && !additionalRootsCanonical?.some((r) => within(r, key))) {
return {
ok: false,
violation: {
code: "invalid",
message: `path "${path}" names a network (UNC) location outside the declared roots; refused even under an open containment face — an access against an undeclared share can hang on the SMB mount timeout and offers credentials to the remote host.`,
},
};
}
return { ok: true, key };
}
if (!within(rootCanonical, key) && !additionalRootsCanonical?.some((r) => within(r, key))) {
if (exactFileReadExemption?.(key) === true)
return { ok: true, key };
const roots = [rootCanonical, ...(additionalRootsCanonical ?? [])];
return {
ok: false,
violation: {
code: "path_not_in_root",
message: `path "${path}" ${key !== path ? `(canonical target: "${key}") ` : ""}resolves outside the allowed root${roots.length > 1 ? "s" : ""} (${roots.join(", ")}); access denied. ${PATH_NOT_IN_ROOT_ESCAPE_HINT}`,
target: key,
roots,
},
};
}
return { ok: true, key };
}
export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
const screened = win32NamespaceScreen(path, family);
if (!screened.ok) {
return { ok: false, message: `path "${clipSpelling(path)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[screened.refuse]}; refused (${screened.refuse}).`, refused: screened.refuse };
}
const unprefixed = nativeUncSpellingOf(family, screened.spelled);
const spelled = family === "posix" && unprefixed.startsWith("//") ? unprefixed.replace(/^\/+/, "/") : unprefixed;
const target = baseCwd ? anchorSpelling(family, baseCwd, spelled) : spelled;
let thrownByProbe;
let seenThrow = false;
const probe = probeRealTarget(env, path, target, family, signal).catch((e) => {
thrownByProbe = e;
seenThrow = true;
throw e;
});
try {
const answer = await withGateTimeout(probe, DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "pathCanonicalization");
return answer.ok ? { ...answer, key: foldTrailingSeparators(answer.key, family) } : answer;
}
catch (err) {
if (seenThrow && err === thrownByProbe)
throw err;
return { ok: false, message: `cannot resolve path "${path}": ${thrownText(err)}` };
}
}
async function probeRealTarget(env, path, target, family, signal) {
const absR = await env.absolutePath(target, signal);
if (!absR.ok)
return { ok: false, message: `cannot resolve path "${path}": ${absR.error.message}` };
let abs = absR.value;
for (let hop = 0; hop < 16; hop++) {
const exists = await env.exists(abs, signal);
if (!exists.ok) {
return { ok: false, message: `cannot determine whether "${path}" exists: ${exists.error.message}`, unresolvedSymlink: true };
}
if (!exists.value) {
const newPath = await canonicalizeNewPath(env, abs, family, signal);
if (!newPath.ok) {
return { ok: false, message: newPath.message, unresolvedSymlink: true };
}
return { ok: true, key: newPath.key };
}
const canon = await env.canonicalPath(abs, signal);
if (canon.ok)
return { ok: true, key: canon.value, aliasResolved: true };
const info = await env.fileInfo(abs, signal);
if (!info.ok) {
return { ok: false, message: `cannot determine whether "${path}" is a symlink: ${info.error.message}`, unresolvedSymlink: true };
}
if (info.value.kind !== "symlink") {
const viaAncestors = await canonicalizeNewPath(env, abs, family, signal, { skipLeaf: true });
if (!viaAncestors.ok) {
return { ok: false, message: viaAncestors.message, unresolvedSymlink: true };
}
return { ok: true, key: viaAncestors.key };
}
if (!env.readLink) {
return { ok: false, message: `cannot resolve symlink "${path}" (env has no readLink)`, unresolvedSymlink: true };
}
const link = await env.readLink(abs, signal);
if (!link.ok) {
return { ok: false, message: `cannot read symlink "${path}": ${link.error.message}`, unresolvedSymlink: true };
}
const screenedTarget = win32NamespaceScreen(link.value, family);
if (!screenedTarget.ok) {
return {
ok: false,
message: `cannot resolve symlink "${path}": its target "${clipSpelling(link.value)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[screenedTarget.refuse]}; refused (${screenedTarget.refuse}).`,
refused: screenedTarget.refuse,
};
}
const hopSpelling = family === "win" ? screenedTarget.spelled : link.value;
const linkTarget = anchorSpelling(family, parentDir(abs), hopSpelling);
const reabs = await env.absolutePath(linkTarget, signal);
if (!reabs.ok) {
return { ok: false, message: `cannot resolve symlink target "${linkTarget}" for "${path}": ${reabs.error.message}`, unresolvedSymlink: true };
}
abs = reabs.value;
}
return { ok: false, message: `symlink chain for "${path}" exceeded the hop cap; target unresolved`, unresolvedSymlink: true };
}
function parentDir(abs) {
const i = lastSepIndex(abs);
if (i <= 0)
return SEP;
const head = abs.slice(0, i);
return /^[A-Za-z]:$/.test(head) ? head + abs[i] : head;
}
function uncAnchorIndex(parts) {
let seen = 0;
for (let i = 0; i < parts.length; i++) {
if (parts[i] === "")
continue;
seen++;
if (seen === 2)
return i;
}
return parts.length;
}
async function canonicalizeNewPath(env, abs, family, signal, opts) {
const sep = nativeSepOf(abs);
const winForm = family === "win" || isWinFormPath(abs);
const parts = winForm ? abs.split(/[\\/]/) : abs.split("/");
const tail = [];
const floor = family === "win" && isUncSpelling(abs) ? uncAnchorIndex(parts) : 1;
if (opts?.skipLeaf === true && parts.length > 1)
tail.push(parts.pop());
while (parts.length > floor) {
const candidate = parts.join(sep) || sep;
const ex = await env.exists(candidate, signal);
if (!ex.ok) {
return { ok: false, message: `cannot determine whether ancestor "${candidate}" exists: ${ex.error.message}` };
}
if (ex.value) {
const canon = await env.canonicalPath(candidate, signal);
if (!canon.ok) {
return { ok: false, message: `cannot canonicalize existing ancestor "${candidate}": ${canon.error.message}` };
}
const tailSegments = tail.reverse();
const canonSep = nativeSepOf(canon.value);
if (tailSegments.length === 0)
return { ok: true, key: canon.value };
return { ok: true, key: [canon.value.replace(family === "win" ? /[\\/]+$/ : /\/+$/, ""), ...tailSegments].join(canonSep) };
}
tail.push(parts.pop());
}
return { ok: true, key: abs };
}
export function violationText(toolName, v) {
return `Error (${toolName}): ${v.message}`;
}
export function violationDetails(v) {
if (v.code === "read_path_denied" && v.target !== undefined && v.pattern !== undefined)
return readPathDeniedCard(v.target, v.pattern);
return v.code === "path_not_in_root" && v.target !== undefined
? { type: "path_not_in_root", code: "path_not_in_root", target: v.target, roots: v.roots ?? [] }
: undefined;
}
export function readPathDeniedCard(target, pattern) {
return { type: "read_path_denied", code: "read_path_denied", target, pattern };
}
export function pathDenialCardOf(evidence) {
return evidence?.face === "read" ? readPathDeniedCard(evidence.view, evidence.rule) : undefined;
}
export function withinAnyRoot(rootsCanonical, p) {
return rootsCanonical.some((r) => within(r, p));
}
export function rootAdmittingArtifactStore(storePhysical, rootsCanonical) {
const storeFamily = pathFamilyOf({ root: storePhysical });
return rootsCanonical.find((root) => pathFamilyOf({ root }) === storeFamily && (withinAnyRoot([root], storePhysical) || withinAnyRoot([storePhysical], root)));
}
export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access; a deployment that wants reads or writes open everywhere can declare readFace / writeFace: \"open\" instead of listing directories.)";
export function requireRead(state, key) {
const entry = state.get(key);
if (entry === undefined || entry.isPartialView) {
return {
code: "not_read",
message: "File has not been read yet. Read it first before writing to it.",
...(entry?.isPartialView ? { partialView: true } : {}),
};
}
return undefined;
}
export const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused — read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
export const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view — the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
export const READ_REFUSED_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead — e.g. `rm` + rewrite, or `iconv`.)";
export function checkNoChange(oldString, newString) {
if (oldString === newString) {
return { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." };
}
return undefined;
}
export function checkStale(entry, currentHash) {
if (entry === undefined || entry.hash !== currentHash) {
return { code: "stale", message: "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it." };
}
return undefined;
}
export function countOccurrences(haystack, needle) {
if (needle === "")
return 0;
return haystack.split(needle).length - 1;
}
export function similarNameSuggestion(siblingNames, missingName) {
const stemOf = (n) => {
const dot = n.lastIndexOf(".");
return dot > 0 ? n.slice(0, dot) : n;
};
const missingLower = missingName.toLowerCase();
const missingStem = stemOf(missingName);
const caseHit = siblingNames.find((n) => n !== missingName && n.toLowerCase() === missingLower);
if (caseHit)
return caseHit;
return siblingNames.find((n) => n !== missingName && stemOf(n) === missingStem);
}
const CURLY_TO_STRAIGHT = { "‘": "'", "’": "'", "“": '"', "”": '"' };
export function normalizeQuotes(s) {
return s.replace(/[‘’“”]/g, (c) => CURLY_TO_STRAIGHT[c] ?? c);
}
export function resolveQuoteMatch(content, oldString) {
if (oldString === "")
return undefined;
if (content.includes(oldString))
return oldString;
const normOld = normalizeQuotes(oldString);
const idx = normalizeQuotes(content).indexOf(normOld);
if (idx === -1)
return undefined;
return content.substring(idx, idx + oldString.length);
}
const UNICODE_ESCAPE_RE = /\\u[0-9a-fA-F]{4}/;
const NON_ASCII_RE = /[\u0080-\uffff]/;
function decodeUnicodeEscapes(s) {
return s.replace(/(\\\\)|\\u([0-9a-fA-F]{4})/g, (m, dbl, hex) => (dbl !== undefined ? m : String.fromCharCode(parseInt(hex, 16))));
}
function escapedSpellingPattern(s) {
let out = "";
for (let i = 0; i < s.length; i++) {
const code = s.charCodeAt(i);
if (code >= 128) {
out += "\\\\u";
for (const d of code.toString(16).padStart(4, "0"))
out += d >= "a" ? `[${d}${d.toUpperCase()}]` : d;
}
else
out += s[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
return out;
}
export function resolveEscapeMatch(content, oldString) {
if (oldString === "")
return undefined;
if (UNICODE_ESCAPE_RE.test(oldString)) {
const decoded = decodeUnicodeEscapes(oldString);
if (decoded !== oldString && content.includes(decoded))
return decoded;
}
if (NON_ASCII_RE.test(oldString)) {
let hit = null;
try {
hit = content.match(new RegExp(escapedSpellingPattern(oldString)));
}
catch {
hit = null;
}
if (hit)
return hit[0];
}
return undefined;
}
export function adaptNewStringEscapes(oldString, matchedOld, newString) {
if (oldString === matchedOld)
return newString;
if (NON_ASCII_RE.test(oldString)) {
let full;
try {
full = new RegExp(`^${escapedSpellingPattern(oldString)}$`);
}
catch {
full = undefined;
}
if (full?.test(matchedOld)) {
const hexOf = new Map();
let upper = 0;
let lower = 0;
for (let i = 0, at = 0; i < oldString.length; i++) {
const code = oldString.charCodeAt(i);
if (code >= 128) {
const hex = matchedOld.slice(at + 2, at + 6);
hexOf.set(code, hex);
for (const d of hex) {
if (d >= "a" && d <= "f")
lower++;
else if (d >= "A" && d <= "F")
upper++;
}
at += 6;
}
else
at += 1;
}
return newString.replace(/[\u0080-\uffff]/g, (ch) => {
const code = ch.charCodeAt(0);
const known = hexOf.get(code);
if (known !== undefined)
return `\\u${known}`;
const hex = code.toString(16).padStart(4, "0");
return `\\u${upper > lower ? hex.toUpperCase() : hex}`;
});
}
}
if (UNICODE_ESCAPE_RE.test(oldString) && decodeUnicodeEscapes(oldString) === matchedOld)
return decodeUnicodeEscapes(newString);
return newString;
}
export function escapeMatchWasAttempted(oldString) {
return UNICODE_ESCAPE_RE.test(oldString) || NON_ASCII_RE.test(oldString);
}
export const ESCAPE_MATCH_MISS_NOTE = "\n(note: Edit also tried swapping \\uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)";
function isQuoteOpenerPosition(chars, i) {
if (i === 0)
return true;
const p = chars[i - 1];
return p === " " || p === "\t" || p === "\n" || p === "\r" || p === "(" || p === "[" || p === "{" || p === "—" || p === "–";
}
export function adaptNewStringQuotes(matchedOld, newString) {
const wantsDouble = matchedOld.includes("“") || matchedOld.includes("”");
const wantsSingle = matchedOld.includes("‘") || matchedOld.includes("’");
if (!wantsDouble && !wantsSingle)
return newString;
const chars = [...newString];
const out = [];
for (let i = 0; i < chars.length; i++) {
const c = chars[i];
if (wantsDouble && c === '"')
out.push(isQuoteOpenerPosition(chars, i) ? "“" : "”");
else if (wantsSingle && c === "'") {
const prev = i > 0 ? chars[i - 1] : undefined;
const next = i < chars.length - 1 ? chars[i + 1] : undefined;
const letterBefore = prev !== undefined && /\p{L}/u.test(prev);
const letterAfter = next !== undefined && /\p{L}/u.test(next);
if (letterBefore && letterAfter)
out.push("’");
else
out.push(isQuoteOpenerPosition(chars, i) ? "‘" : "’");
}
else
out.push(c);
}
return out.join("");
}
export function deletionOldString(content, oldString, newString) {
if (newString !== "")
return oldString;
if (!oldString.endsWith("\n") && content.includes(oldString + "\n"))
return oldString + "\n";
return oldString;
}
export const EDIT_ECHO_MAX_CHARS = 200;
function boundEditEcho(oldString) {
if (oldString.length <= EDIT_ECHO_MAX_CHARS)
return oldString;
return `${sliceHeadSafe(oldString, EDIT_ECHO_MAX_CHARS)}… (truncated)`;
}
export function checkEditMatch(content, oldString, replaceAll, truncated) {
if (oldString === "") {
return { code: "invalid", message: "old_string must not be empty." };
}
const n = countOccurrences(content, oldString);
if (n === 0) {
return {
code: "ambiguous_edit",
message: `String to replace not found in file.${truncated ? " (note: the read was truncated — it may be in the unshown portion)" : ""}\nString: ${boundEditEcho(oldString)}`,
};
}
if (n > 1 && !replaceAll) {
return {
code: "ambiguous_edit",
message: `Found ${n} matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.${truncated ? " (the read was truncated; some matches may be in the unshown portion)" : ""}\nString: ${boundEditEcho(oldString)}`,
};
}
return undefined;
}