openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
555 lines (554 loc) • 28.8 kB
JavaScript
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { n as isErrno, t as hasErrnoCode } from "./errno-CkbDOfLk.js";
import { D as walkDirectory, o as ensureAbsoluteDirectory, w as root } from "./fs-safe-B6pvPGnf.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { E as tableExists, S as OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { r as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BRgmrGHt.js";
import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-K6J_UQGL.js";
import { r as SKILL_LIBRARY_MAX_FILE_BYTES } from "./skill-library-s2fFtoG-.js";
import { a as selectResolvedUserProfile, c as userProfilesDb, o as selectResolvedUserProfileById } from "./user-profiles-internal-D0HRUN8X.js";
import { n as createSyntheticSourceInfo } from "./source-info-CcFiWAof.js";
import { i as resolveSkillManifestMetadata, n as resolveSkillInvocationPolicy, t as parseSkillFrontmatter } from "./frontmatter-CrPBFeiv.js";
import { i as resolveSkillDisplayName } from "./skill-contract-CcQSbxgS.js";
import { c as resolveOperatorRolePolicyForAssignment } from "./operator-role-policy-wsr1DeJv.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { Type } from "typebox";
import { Value } from "typebox/value";
function sanitizeSkillCommandName(raw) {
return normalizeLowercaseStringOrEmpty(raw).replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "skill";
}
//#endregion
//#region src/skills/library/command-name.ts
/** Fits native 32-character limits without truncating the stable 80-bit identity suffix. */
function managedSkillCommandName(slug, skillId) {
return `s_${slug.replace(/-+/g, "_").slice(0, 9).replace(/_+$/, "")}_${skillId.replaceAll("-", "").slice(0, 20)}`;
}
/** Explicit references must never resolve a different bundle that copied a managed command name. */
function assertUnambiguousManagedSkillNames(entries) {
const managed = new Set(entries.filter((entry) => entry.skill.source === "openclaw-library").map((entry) => entry.skill.name));
if (!managed.size) return;
const seen = /* @__PURE__ */ new Set();
for (const entry of entries) {
const name = sanitizeSkillCommandName(entry.skill.name);
if (managed.has(name) && seen.has(name)) throw new Error(`Skill command ${name} is ambiguous. Rename the conflicting workspace skill or detach the managed skill before retrying.`);
seen.add(name);
}
}
//#endregion
//#region src/skills/library/errors.ts
var SkillLibraryError = class extends Error {
constructor(code, message, currentRevision, options) {
super(message, options);
this.code = code;
this.currentRevision = currentRevision;
this.name = "SkillLibraryError";
}
};
const SKILL_LIBRARY_MAX_TREE_ENTRIES = 512;
/** Identifies the exact directory failure that prevented complete skill-tree traversal. */
var SkillTreeDirectoryError = class extends SkillLibraryError {
constructor(rootPath, failedPath, cause) {
super("INVALID_BUNDLE", `Skill tree directory could not be read: root=${JSON.stringify(rootPath)} path=${JSON.stringify(failedPath)} error=${describeSkillTreeFailure(cause)}`, void 0, { cause });
this.rootPath = rootPath;
this.failedPath = failedPath;
}
};
const portableCompare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
const manifestSchema = Type.Array(Type.Object({
path: Type.String({ maxLength: 512 }),
sha256: Type.String({ pattern: "^[a-f0-9]{64}$" }),
sizeBytes: Type.Integer({
minimum: 0,
maximum: SKILL_LIBRARY_MAX_FILE_BYTES
}),
executable: Type.Boolean()
}, { additionalProperties: false }), {
minItems: 1,
maxItems: 256
});
/** Published executable flags are portable metadata; host mode bits are not their authority. */
async function readSkillLibraryManifestTree(directory, manifestJson, revision) {
const manifest = JSON.parse(manifestJson);
if (!Value.Check(manifestSchema, manifest)) throw new SkillLibraryError("INVALID_BUNDLE", "Published skill manifest is invalid.");
const safeRoot = await root(directory);
const files = [];
let total = 0;
for (const file of manifest) {
validateSkillLibraryPath(file.path);
total += file.sizeBytes;
if (total > 8388608) throw new SkillLibraryError("INVALID_BUNDLE", "Published skill manifest exceeds bundle limits.");
const { buffer } = await safeRoot.read(file.path, {
hardlinks: "reject",
symlinks: "reject",
maxBytes: file.sizeBytes
});
if (buffer.length !== file.sizeBytes || sha256(buffer) !== file.sha256) throw new SkillLibraryError("INVALID_BUNDLE", `Published skill file failed integrity verification: ${file.path}`);
files.push({
path: file.path,
content: buffer.toString("base64"),
encoding: "base64",
executable: file.executable
});
}
if (prepareSkillLibraryBundle(files).revision !== revision) throw new SkillLibraryError("INVALID_BUNDLE", "Published skill revision failed integrity verification.");
return files;
}
function validateSkillLibraryPath(filePath) {
validateSkillBundlePath(filePath);
if (filePath.split("/").some((part) => [
".git",
"node_modules",
".openclaw"
].includes(part.toLowerCase()))) throw new SkillLibraryError("INVALID_BUNDLE", `Non-portable skill file path: ${filePath}`);
}
function validateSkillBundlePath(filePath) {
const parts = filePath.split("/");
if (filePath.length > 512 || Buffer.from(filePath, "utf8").toString("utf8") !== filePath || parts.length > 16 || parts.some((part) => !part || part === "." || part === ".." || /[\\<>:"|?*]/u.test(part) || Array.from(part).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) || /[ .]$/u.test(part) || part !== part.normalize("NFC") || /^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu.test(part) || /^(conin|conout)\$$/iu.test(part))) throw new SkillLibraryError("INVALID_BUNDLE", `Non-portable skill file path: ${filePath}`);
}
function decodeSkillLibraryFile(file) {
const bytes = Buffer.from(file.content, file.encoding === "base64" ? "base64" : "utf8");
if (file.encoding === "base64" && bytes.toString("base64") !== file.content) throw new SkillLibraryError("INVALID_BUNDLE", `Invalid base64: ${file.path}`);
return bytes;
}
/** Validate exact portable artifacts without imposing publication metadata on loaded skills. */
function prepareSkillBundle(files) {
if (files.length > 256) throw new SkillLibraryError("INVALID_BUNDLE", "Skill bundle exceeds 256 files.");
const paths = /* @__PURE__ */ new Set();
let total = 0;
const prepared = files.map((file) => {
validateSkillBundlePath(file.path);
const folded = file.path.toLowerCase();
if (paths.has(folded)) throw new SkillLibraryError("INVALID_BUNDLE", `Duplicate skill file: ${file.path}`);
paths.add(folded);
const bytes = decodeSkillLibraryFile(file);
total += bytes.length;
if (bytes.length > 1048576 || total > 8388608) throw new SkillLibraryError("INVALID_BUNDLE", "Skill bundle exceeds file (1 MiB) or total (8 MiB) limit.");
return {
path: file.path,
bytes,
sha256: sha256(bytes),
sizeBytes: bytes.length,
executable: file.executable === true
};
}).toSorted((a, b) => portableCompare(a.path, b.path));
for (const file of prepared) {
const parts = file.path.toLowerCase().split("/");
parts.pop();
while (parts.length) {
if (paths.has(parts.join("/"))) throw new SkillLibraryError("INVALID_BUNDLE", `File/directory collision: ${file.path}`);
parts.pop();
}
}
const skillMd = prepared.find((file) => file.path === "SKILL.md");
if (!skillMd || !Buffer.from(skillMd.bytes.toString("utf8")).equals(skillMd.bytes)) throw new SkillLibraryError("INVALID_BUNDLE", "Bundle requires a UTF-8 SKILL.md.");
const manifest = prepared.map(({ bytes: _bytes, ...file }) => file);
return {
revision: sha256(JSON.stringify(["openclaw.skill-library.tree.v1", manifest])),
files: prepared
};
}
function prepareSkillLibraryBundle(files) {
const bundle = prepareSkillBundle(files);
for (const file of bundle.files) validateSkillLibraryPath(file.path);
const frontmatter = parseSkillFrontmatter(bundle.files.find((file) => file.path === "SKILL.md").bytes.toString("utf8"));
if (!frontmatter.name?.trim() || !frontmatter.description?.trim() || frontmatter.description.length > 1024) throw new SkillLibraryError("INVALID_BUNDLE", "SKILL.md requires name and description (at most 1,024 characters).");
return {
...bundle,
description: frontmatter.description
};
}
function skillLibraryRevisionDir(skillId, revision, env) {
if (!/^[a-f0-9-]{36}$/u.test(skillId) || !/^[a-f0-9]{64}$/u.test(revision)) throw new SkillLibraryError("INVALID_BUNDLE", "Invalid skill revision reference.");
return path.join(resolveStateDir(env), "skill-library", skillId, "revisions", revision);
}
async function syncDirectory(directory) {
try {
const handle = await fs$1.open(directory, "r");
try {
await handle.sync();
} finally {
await handle.close();
}
} catch (error) {
if (!isErrno(error) || ![
"EINVAL",
"ENOTSUP",
"EISDIR",
"EPERM"
].includes(error.code ?? "")) throw error;
}
}
async function cleanAbandonedSkillStaging(parent) {
for (const entry of await fs$1.readdir(parent, { withFileTypes: true })) {
const match = /^\.staging-([0-9]+)-[a-zA-Z0-9]+$/u.exec(entry.name);
if (!match || !entry.isDirectory()) continue;
const staging = path.join(parent, entry.name);
const stat = await fs$1.lstat(staging);
if (Date.now() - stat.mtimeMs < 36e5) continue;
try {
process.kill(Number(match[1]), 0);
} catch (error) {
if (hasErrnoCode(error, "ESRCH")) await fs$1.rm(staging, {
recursive: true,
force: true
});
}
}
}
async function stageSkillLibraryBundle(skillId, bundle, env) {
const destination = skillLibraryRevisionDir(skillId, bundle.revision, env);
const parent = path.dirname(destination);
const ensured = await ensureAbsoluteDirectory(parent, { mode: 448 });
if (!ensured.ok) throw ensured.error;
await cleanAbandonedSkillStaging(parent);
const staging = await fs$1.mkdtemp(path.join(parent, `.staging-${process.pid}-`));
try {
const directories = /* @__PURE__ */ new Set([staging]);
for (const file of bundle.files) {
const target = path.join(staging, file.path);
await fs$1.mkdir(path.dirname(target), {
recursive: true,
mode: 448
});
let directory = path.dirname(target);
while (directory !== parent) {
directories.add(directory);
directory = path.dirname(directory);
}
const handle = await fs$1.open(target, "wx", file.executable ? 320 : 256);
try {
await handle.writeFile(file.bytes);
await handle.sync();
} finally {
await handle.close();
}
}
for (const directory of [...directories].toSorted((a, b) => b.length - a.length)) await syncDirectory(directory);
return {
staging,
async publish() {
try {
await fs$1.rename(staging, destination);
} catch (error) {
if (!isErrno(error) || !["EEXIST", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
await readSkillLibraryManifestTree(destination, JSON.stringify(bundle.files.map(({ bytes: _bytes, ...file }) => file)), bundle.revision);
}
await syncDirectory(parent);
await syncDirectory(path.dirname(parent));
await syncDirectory(path.dirname(path.dirname(parent)));
await syncDirectory(path.dirname(path.dirname(path.dirname(parent))));
return destination;
},
async cleanup() {
await fs$1.rm(staging, {
recursive: true,
force: true
});
}
};
} catch (error) {
await fs$1.rm(staging, {
recursive: true,
force: true
});
throw error;
}
}
async function readSkillLibraryTree(directory) {
const files = await readSkillBundleTree(directory);
for (const file of files) validateSkillLibraryPath(file.path);
return files;
}
function describeSkillTreeFailure(error) {
if (isErrno(error) && error.code) return `${error.code}: ${error.message}`;
return error instanceof Error ? error.message : String(error);
}
async function readSkillBundleTree(directory, includePath) {
const include = includePath ? (entry) => includePath(entry.path) : void 0;
const walked = await walkDirectory(directory, {
maxDepth: 17,
maxEntries: SKILL_LIBRARY_MAX_TREE_ENTRIES,
symlinks: "include",
include,
descend: include
});
if (walked.truncated || walked.entries.some((entry) => entry.depth > 16)) throw new SkillLibraryError("INVALID_BUNDLE", "Skill tree exceeds traversal limits.");
if (walked.failedDirs.length) {
const failed = walked.failedDirs[0];
throw new SkillTreeDirectoryError(directory, failed.path, failed.error);
}
const safeRoot = await root(directory).catch((error) => {
throw new SkillTreeDirectoryError(directory, directory, error);
});
const files = [];
let total = 0;
for (const entry of walked.entries) {
if (entry.kind === "directory") continue;
if (entry.kind !== "file") throw new SkillLibraryError("INVALID_BUNDLE", `Skill trees cannot contain links or special files: root=${JSON.stringify(directory)} path=${JSON.stringify(entry.path)} kind=${entry.kind}.`);
const portablePath = entry.relativePath.split(path.sep).join("/");
validateSkillBundlePath(portablePath);
const { buffer, stat } = await safeRoot.read(entry.relativePath, {
hardlinks: "reject",
symlinks: "reject",
maxBytes: SKILL_LIBRARY_MAX_FILE_BYTES
}).catch((error) => {
throw new SkillLibraryError("INVALID_BUNDLE", `Skill tree file could not be read: root=${JSON.stringify(directory)} path=${JSON.stringify(entry.path)} error=${describeSkillTreeFailure(error)}`, void 0, { cause: error });
});
total += buffer.length;
if (total > 8388608 || files.length >= 256) throw new SkillLibraryError("INVALID_BUNDLE", "Skill tree exceeds bundle limits.");
files.push({
path: portablePath,
content: buffer.toString("base64"),
encoding: "base64",
executable: (stat.mode & 73) !== 0
});
}
return files.toSorted((a, b) => portableCompare(a.path, b.path));
}
//#endregion
//#region src/skills/library/store.ts
const skillLibraryDb = (db) => getNodeSqliteKysely(db);
const ensured = /* @__PURE__ */ new WeakSet();
function ensureSkillLibrarySchema(options) {
const { db } = openOpenClawStateDatabase(options);
if (ensured.has(db)) return;
const start = OPENCLAW_STATE_SCHEMA_SQL.indexOf("CREATE TABLE IF NOT EXISTS skill_library_entries (");
const end = OPENCLAW_STATE_SCHEMA_SQL.indexOf("-- End profile-owned skill library.", start);
if (start < 0 || end < start) throw new Error("Canonical skill library schema missing.");
runOpenClawStateWriteTransaction(({ db: transactionDb }) => {
transactionDb.exec(OPENCLAW_STATE_SCHEMA_SQL.slice(start, end));
}, options, { operationLabel: "skills.library.schema" });
ensured.add(db);
}
function readSkillLibraryStore(read, options) {
if (options.database) return tableExists(options.database.db, "skill_library_entries") ? read(options.database.db) : void 0;
return withExistingOpenClawStateDatabaseReadOnly(({ db }) => tableExists(db, "skill_library_entries") ? read(db) : void 0, options);
}
function resolveSkillLibraryActor(db, authority) {
authority.assertCurrent();
const profile = authority.profileId && tableExists(db, "user_profiles") ? selectResolvedUserProfileById(db, authority.profileId) : void 0;
if (authority.profileId && !profile) throw new SkillLibraryError("AUTHORITY_EXPIRED", "Your Gateway profile is no longer available. Sign in again before accessing the library.");
const ceiling = resolveOperatorRolePolicyForAssignment(profile?.id, profile?.role ?? null, authority.getConfig())?.scopes;
const permits = (scope) => authorizeOperatorScopesForRequiredScope(scope, [...authority.scopes]).allowed && (!ceiling || authorizeOperatorScopesForRequiredScope(scope, ceiling).allowed);
return {
profileId: profile?.id,
admin: permits("operator.admin"),
read: permits("operator.read") || permits("operator.write"),
write: Boolean(profile) && permits("operator.write")
};
}
function requireSkillLibraryProfile(db, authority) {
const actor = resolveSkillLibraryActor(db, authority);
if (!actor.profileId) throw new SkillLibraryError("IDENTITY_REQUIRED", "Sign in with a durable Gateway profile to use a personal skill library. Shared-token administrators can use workspace skills.");
if (!actor.write) throw new SkillLibraryError("FORBIDDEN", "Your current Gateway role cannot change skills.");
return actor.profileId;
}
function requireSkillLibraryUpload(db, uploadId, authority) {
const actor = requireSkillLibraryProfile(db, authority);
const upload = executeSqliteQueryTakeFirstSync(db, skillLibraryDb(db).selectFrom("skill_library_uploads").selectAll().where("upload_id", "=", uploadId));
if (!upload || upload.expires_at <= Date.now() || selectSkillLibraryOwner(db, upload.owner_profile_id)?.id !== actor) throw new SkillLibraryError("NOT_FOUND", "Upload not found for your profile, or expired. Start a new import.");
return upload;
}
function selectSkillLibraryRow(db, skillId) {
return executeSqliteQueryTakeFirstSync(db, skillLibraryDb(db).selectFrom("skill_library_entries").selectAll().where("skill_id", "=", skillId));
}
function skillLibraryRevisionQuery(db, skillId, revision) {
return skillLibraryDb(db).selectFrom("skill_library_revisions").where("skill_id", "=", skillId).where("revision", "=", revision);
}
function selectSkillLibraryRevision(db, skillId, revision) {
return executeSqliteQueryTakeFirstSync(db, skillLibraryRevisionQuery(db, skillId, revision).selectAll());
}
function selectSkillLibraryRevisionMetadata(db, skillId, revision) {
return executeSqliteQueryTakeFirstSync(db, skillLibraryRevisionQuery(db, skillId, revision).select("description"));
}
function selectSkillLibraryOwner(db, profileId) {
return selectResolvedUserProfile(db, profileId, userProfilesDb(db).selectFrom("user_profiles").select([
"id",
"display_name",
"merged_into"
]));
}
function canonicalOwner(db, owner) {
return owner && tableExists(db, "user_profiles") ? selectSkillLibraryOwner(db, owner)?.id ?? owner : owner;
}
function projectSkillLibraryEntry(db, row, authority, revision = row.current_revision, selectedBySession = false) {
const actor = resolveSkillLibraryActor(db, authority);
const owner = canonicalOwner(db, row.owner_profile_id);
if (!actor.read || !selectedBySession && !actor.admin && owner !== actor.profileId && !row.shared && owner !== null) return;
const metadata = selectSkillLibraryRevisionMetadata(db, row.skill_id, revision);
if (!metadata) return;
return {
skillId: row.skill_id,
slug: row.slug,
name: managedSkillCommandName(row.slug, row.skill_id),
ownerLabel: owner === null ? "Team" : selectSkillLibraryOwner(db, owner)?.display_name ?? owner,
description: metadata.description,
ownerProfileId: owner,
authorProfileId: row.author_profile_id,
shared: row.shared === 1,
enabled: row.enabled === 1,
removed: row.removed === 1,
revision,
createdAt: row.created_at,
updatedAt: row.updated_at,
canEdit: actor.write && (authority.namespace !== "personal" && actor.admin || owner !== null && actor.profileId === owner)
};
}
function requireSkillLibraryEntry(db, skillId, authority, write = false) {
const row = selectSkillLibraryRow(db, skillId);
const entry = row && projectSkillLibraryEntry(db, row, authority);
if (!entry) throw new SkillLibraryError("NOT_FOUND", "Skill not found in your accessible library.");
if (write && !entry.canEdit) {
requireSkillLibraryProfile(db, authority);
throw new SkillLibraryError("FORBIDDEN", authority.namespace === "personal" ? "Personal authoring can change only your own skills. Use the administrator UI or CLI for team management." : "Only the skill's owner or a Gateway administrator can change it.");
}
if (write && entry.removed) throw new SkillLibraryError("NOT_FOUND", "Removed skills cannot be edited or selected again; pinned revisions remain available to their sessions.");
return entry;
}
function assertSkillLibraryRevision(entry, expected) {
if (entry.revision !== expected) throw new SkillLibraryError("CONFLICT", "Skill changed. Read the current revision and review your edit before saving again.", entry.revision);
}
function assertSkillLibraryNameAvailable(db, owner, slug, exceptId) {
if (executeSqliteQuerySync(db, skillLibraryDb(db).selectFrom("skill_library_entries").selectAll().where("slug", "=", slug).where("removed", "=", 0)).rows.some((row) => row.skill_id !== exceptId && canonicalOwner(db, row.owner_profile_id) === owner)) throw new SkillLibraryError("NAME_CONFLICT", `A skill named "${slug}" already exists in this library. Choose a different slug; existing skills were preserved.`);
}
function recordSkillLibraryEvent(db, skillId, revision, action, actorProfileId) {
executeSqliteQuerySync(db, skillLibraryDb(db).insertInto("skill_library_events").values({
event_id: randomUUID(),
skill_id: skillId,
revision,
action,
actor_profile_id: actorProfileId,
created_at: Date.now()
}));
}
//#endregion
//#region src/skills/library/selection.ts
const preparedSelections = /* @__PURE__ */ new WeakMap();
/** Only uncommitted human seeds carry this closure. Persisted session pins intentionally do not. */
function assertPreparedSkillLibrarySelection(selections) {
if (selections) preparedSelections.get(selections)?.();
}
const selectedEntryCache = /* @__PURE__ */ new Map();
/** The session owner has already authorized this exact immutable pin. */
async function readSelectedSkillLibraryFiles(selection, options = {}) {
const metadata = readSkillLibraryStore((db) => selectSkillLibraryRevision(db, selection.skillId, selection.revision), options);
if (!metadata) throw new SkillLibraryError("NOT_FOUND", "Selected skill revision is unavailable.");
return await readSkillLibraryManifestTree(skillLibraryRevisionDir(selection.skillId, selection.revision, options.env), metadata.files_json, selection.revision);
}
/** Called only by a fresh human-session admission, never from creator/assignee attribution. */
function seedSkillLibrarySelection(authority, options = {}) {
if (!authority.profileId) return [];
const result = readSkillLibraryStore((db) => {
const actor = resolveSkillLibraryActor(db, authority);
if (!actor.profileId) return [];
return executeSqliteQuerySync(db, skillLibraryDb(db).selectFrom("skill_library_entries").selectAll().where("removed", "=", 0).where("enabled", "=", 1).orderBy("skill_id")).rows.flatMap((row) => {
const entry = projectSkillLibraryEntry(db, row, authority);
if (!entry || entry.ownerProfileId !== actor.profileId && entry.ownerProfileId !== null && !entry.shared) return [];
return [{
skillId: entry.skillId,
revision: entry.revision,
name: entry.name,
ownerProfileId: entry.ownerProfileId
}];
}).toSorted((a, b) => Number(b.ownerProfileId === actor.profileId) - Number(a.ownerProfileId === actor.profileId)).slice(0, 64);
}, options) ?? [];
if (result.length) preparedSelections.set(result, () => {
authority.assertCurrent();
if (!readSkillLibraryStore((db) => {
for (const pin of result) {
const entry = requireSkillLibraryEntry(db, pin.skillId, authority);
if (entry.removed || !entry.enabled || !selectSkillLibraryRevisionMetadata(db, pin.skillId, pin.revision)) throw new SkillLibraryError("CONFLICT", "Default skill access changed during session creation. Retry to select current defaults.");
}
return true;
}, options)) throw new SkillLibraryError("CONFLICT", "Default skill library changed during session creation; retry.");
});
return result;
}
/** Session mutation authorization is separate; this checks the collaborator's library access. */
function changeSkillLibrarySelection(authority, current, params, options = {}) {
if (params.action !== "refresh" && !params.skillId) throw new SkillLibraryError("INVALID_BUNDLE", "attach/detach requires skillId.");
if (params.action === "detach") {
authority.assertCurrent();
return current.filter((item) => item.skillId !== params.skillId);
}
const result = readSkillLibraryStore((db) => {
const next = new Map(current.map((item) => [item.skillId, item]));
const ids = params.skillId ? [params.skillId] : current.map((item) => item.skillId);
for (const skillId of ids) {
const entry = requireSkillLibraryEntry(db, skillId, authority);
if (entry.removed) throw new SkillLibraryError("NOT_FOUND", "Removed skill cannot be selected. Existing pinned selections remain available.");
const revision = params.revision ?? entry.revision;
if (!selectSkillLibraryRevisionMetadata(db, skillId, revision)) throw new SkillLibraryError("NOT_FOUND", "Skill revision not found.");
next.set(skillId, {
skillId,
revision,
name: entry.name,
ownerProfileId: entry.ownerProfileId
});
}
if (next.size > 64) throw new SkillLibraryError("LIMIT", "A session can select at most 64 library skills.");
return [...next.values()].toSorted((a, b) => a.skillId < b.skillId ? -1 : a.skillId > b.skillId ? 1 : 0);
}, options);
if (!result) throw new SkillLibraryError("NOT_FOUND", "Skill library is empty.");
return result;
}
/** Resolve already-authorized pins only when rebuilding a snapshot, independent of current sharing. */
function loadSkillLibrarySelection(selections, options = {}) {
if (!selections.length) return [];
const cacheKey = JSON.stringify([
resolveStateDir(options.env),
options.path,
selections
]);
const cached = selectedEntryCache.get(cacheKey);
if (cached) return [...cached];
if (selections.length > 64) throw new SkillLibraryError("LIMIT", "Invalid session skill selection.");
const entries = readSkillLibraryStore((db) => selections.map((selection) => {
const revision = selectSkillLibraryRevisionMetadata(db, selection.skillId, selection.revision);
if (!revision) throw new SkillLibraryError("NOT_FOUND", "A pinned skill revision is unavailable; restore the library artifact or detach it explicitly.");
const baseDir = skillLibraryRevisionDir(selection.skillId, selection.revision, options.env);
const filePath = path.join(baseDir, "SKILL.md");
const content = fs.readFileSync(filePath, "utf8");
const frontmatter = parseSkillFrontmatter(content);
const metadata = resolveSkillManifestMetadata(frontmatter);
const invocation = resolveSkillInvocationPolicy(frontmatter);
const name = selection.name;
return {
skill: {
name,
displayName: resolveSkillDisplayName(content, frontmatter.name ?? name),
description: revision.description,
baseDir,
filePath,
source: "openclaw-library",
sourceInfo: createSyntheticSourceInfo(filePath, {
source: "openclaw-library",
baseDir
}),
disableModelInvocation: invocation.disableModelInvocation
},
frontmatter,
invocation,
metadata: {
skillKey: name,
os: metadata?.os,
requires: metadata?.requires
},
disableCommandDispatch: true,
syncSourceDir: baseDir,
syncDirName: `library-${selection.skillId}-${selection.revision}`
};
}), options);
if (!entries) throw new SkillLibraryError("NOT_FOUND", "Pinned skill library is unavailable; restore it before running this session.");
selectedEntryCache.set(cacheKey, entries);
if (selectedEntryCache.size > 32) selectedEntryCache.delete(selectedEntryCache.keys().next().value);
return [...entries];
}
//#endregion
export { stageSkillLibraryBundle as A, decodeSkillLibraryFile as C, readSkillLibraryManifestTree as D, readSkillBundleTree as E, SkillLibraryError as M, assertUnambiguousManagedSkillNames as N, readSkillLibraryTree as O, sanitizeSkillCommandName as P, SkillTreeDirectoryError as S, prepareSkillLibraryBundle as T, selectSkillLibraryRevision as _, seedSkillLibrarySelection as a, skillLibraryDb as b, ensureSkillLibrarySchema as c, recordSkillLibraryEvent as d, requireSkillLibraryEntry as f, selectSkillLibraryOwner as g, resolveSkillLibraryActor as h, readSelectedSkillLibraryFiles as i, validateSkillLibraryPath as j, skillLibraryRevisionDir as k, projectSkillLibraryEntry as l, requireSkillLibraryUpload as m, changeSkillLibrarySelection as n, assertSkillLibraryNameAvailable as o, requireSkillLibraryProfile as p, loadSkillLibrarySelection as r, assertSkillLibraryRevision as s, assertPreparedSkillLibrarySelection as t, readSkillLibraryStore as u, selectSkillLibraryRevisionMetadata as v, prepareSkillBundle as w, SKILL_LIBRARY_MAX_TREE_ENTRIES as x, selectSkillLibraryRow as y };