Files
openclaw/src/agents/skills/local-loader.ts
Peter Steinberger 538605ff44 [codex] Extract filesystem safety primitives (#77918)
* refactor: extract filesystem safety primitives

* refactor: use fs-safe for file access helpers

* refactor: reuse fs-safe for media reads

* refactor: use fs-safe for image reads

* refactor: reuse fs-safe in qqbot media opener

* refactor: reuse fs-safe for local media checks

* refactor: consume cleaner fs-safe api

* refactor: align fs-safe json option names

* fix: preserve fs-safe migration contracts

* refactor: use fs-safe primitive subpaths

* refactor: use grouped fs-safe subpaths

* refactor: align fs-safe api usage

* refactor: adapt private state store api

* chore: refresh proof gate

* refactor: follow fs-safe json api split

* refactor: follow reduced fs-safe surface

* build: default fs-safe python helper off

* fix: preserve fs-safe plugin sdk aliases

* refactor: consolidate fs-safe usage

* refactor: unify fs-safe store usage

* refactor: trim fs-safe temp workspace usage

* refactor: hide low-level fs-safe primitives

* build: use published fs-safe package

* fix: preserve outbound recovery durability after rebase

* chore: refresh pr checks
2026-05-06 02:15:17 +01:00

173 lines
4.3 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { openRootFileSync } from "../../infra/boundary-file-read.js";
import { parseFrontmatter, resolveSkillInvocationPolicy } from "./frontmatter.js";
import { createSyntheticSourceInfo, type Skill } from "./skill-contract.js";
import type { ParsedSkillFrontmatter } from "./types.js";
type LoadedLocalSkill = {
skill: Skill;
frontmatter: ParsedSkillFrontmatter;
};
function readSkillFileSync(params: {
rootRealPath: string;
filePath: string;
maxBytes?: number;
}): string | null {
const opened = openRootFileSync({
absolutePath: params.filePath,
rootPath: params.rootRealPath,
rootRealPath: params.rootRealPath,
boundaryLabel: "skill root",
maxBytes: params.maxBytes,
});
if (!opened.ok) {
return null;
}
try {
return fs.readFileSync(opened.fd, "utf8");
} finally {
fs.closeSync(opened.fd);
}
}
function loadSingleSkillDirectory(params: {
skillDir: string;
source: string;
rootRealPath: string;
maxBytes?: number;
}): LoadedLocalSkill | null {
const skillFilePath = path.join(params.skillDir, "SKILL.md");
const raw = readSkillFileSync({
rootRealPath: params.rootRealPath,
filePath: skillFilePath,
maxBytes: params.maxBytes,
});
if (!raw) {
return null;
}
let frontmatter: Record<string, string>;
try {
frontmatter = parseFrontmatter(raw);
} catch {
return null;
}
const fallbackName = path.basename(params.skillDir).trim();
const name = frontmatter.name?.trim() || fallbackName;
const description = frontmatter.description?.trim();
if (!name || !description) {
return null;
}
const invocation = resolveSkillInvocationPolicy(frontmatter);
const filePath = path.resolve(skillFilePath);
const baseDir = path.resolve(params.skillDir);
return {
skill: {
name,
description,
filePath,
baseDir,
source: params.source,
sourceInfo: createSyntheticSourceInfo(filePath, {
source: params.source,
baseDir,
scope: "project",
origin: "top-level",
}),
disableModelInvocation: invocation.disableModelInvocation,
},
frontmatter,
};
}
function listCandidateSkillDirs(dir: string): string[] {
try {
return fs
.readdirSync(dir, { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules",
)
.map((entry) => path.join(dir, entry.name))
.sort((left, right) => left.localeCompare(right));
} catch {
return [];
}
}
export function loadSkillsFromDirSafe(params: { dir: string; source: string; maxBytes?: number }): {
skills: Skill[];
frontmatterByFilePath: ReadonlyMap<string, ParsedSkillFrontmatter>;
} {
const rootDir = path.resolve(params.dir);
let rootRealPath: string;
try {
rootRealPath = fs.realpathSync(rootDir);
} catch {
return { skills: [], frontmatterByFilePath: new Map() };
}
const rootSkill = loadSingleSkillDirectory({
skillDir: rootDir,
source: params.source,
rootRealPath,
maxBytes: params.maxBytes,
});
if (rootSkill) {
return {
skills: [rootSkill.skill],
frontmatterByFilePath: new Map([[rootSkill.skill.filePath, rootSkill.frontmatter]]),
};
}
const loadedSkills = listCandidateSkillDirs(rootDir)
.map((skillDir) =>
loadSingleSkillDirectory({
skillDir,
source: params.source,
rootRealPath,
maxBytes: params.maxBytes,
}),
)
.filter((skill): skill is LoadedLocalSkill => skill !== null);
const frontmatterByFilePath = new Map<string, ParsedSkillFrontmatter>();
for (const loaded of loadedSkills) {
frontmatterByFilePath.set(loaded.skill.filePath, loaded.frontmatter);
}
return {
skills: loadedSkills.map((loaded) => loaded.skill),
frontmatterByFilePath,
};
}
export function readSkillFrontmatterSafe(params: {
rootDir: string;
filePath: string;
maxBytes?: number;
}): Record<string, string> | null {
let rootRealPath: string;
try {
rootRealPath = fs.realpathSync(path.resolve(params.rootDir));
} catch {
return null;
}
const raw = readSkillFileSync({
rootRealPath,
filePath: path.resolve(params.filePath),
maxBytes: params.maxBytes,
});
if (!raw) {
return null;
}
try {
return parseFrontmatter(raw);
} catch {
return null;
}
}