openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,073 lines (1,072 loc) • 42.8 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { C as resolveExpiresAtMsFromDurationMs, m as isFutureDateTimestampMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as createAsyncLock } from "./async-lock-CaiUOILd.js";
import { f as fetchClawHubSkillSecurityVerdicts, u as fetchClawHubSkillDetail, v as resolveClawHubBaseUrl } from "./clawhub-CzBrlwb2.js";
import { m as writeJson, t as readDurableJsonFile } from "./json-files-2umMHm0W.js";
import "./agent-scope-MrLta7Pq.js";
import { u as normalizeAgentId } from "./session-key-B_NoIfpX.js";
import { c as resolveDefaultAgentId, n as listAgentIds, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js";
import "./archive-Dcpo6Wva.js";
import "./archive-CBe_wA_B.js";
import { s as loadWorkspaceSkillEntries } from "./workspace-_gXfieOh.js";
import { n as redactConfigObject } from "./redact-snapshot-CuoBedjD.js";
import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js";
import { _n as validateSkillsStatusParams, an as validateSkillsDetailParams, bn as validateSkillsUploadChunkParams, cn as validateSkillsProposalCreateParams, dn as validateSkillsProposalReviseParams, fn as validateSkillsProposalUpdateParams, gn as validateSkillsSkillCardParams, hn as validateSkillsSecurityVerdictsParams, in as validateSkillsBinsParams, ln as validateSkillsProposalInspectParams, mn as validateSkillsSearchParams, on as validateSkillsInstallParams, pn as validateSkillsProposalsListParams, sn as validateSkillsProposalActionParams, t as formatValidationErrors, un as validateSkillsProposalRequestRevisionParams, vn as validateSkillsUpdateParams, xn as validateSkillsUploadCommitParams, yn as validateSkillsUploadBeginParams } from "./src-oj0IwW6K.js";
import { d as installSkillArchiveFromPath, f as validateRequestedSkillSlug, i as readLocalSkillCardContentSync, l as updateSkillsFromClawHub, r as installSkillFromClawHub, s as searchSkillsFromClawHub, t as buildWorkspaceSkillStatus } from "./status-Dlj2eT9o.js";
import { a as proposeUpdateSkill, d as reviseSkillProposal, i as proposeCreateSkill, l as rejectSkillProposal, n as inspectSkillProposal, o as quarantineSkillProposal, r as listSkillProposals, t as applySkillProposal } from "./service-BlZrnDY7.js";
import { t as canExecRequestNode } from "./exec-defaults-BenGbA5f.js";
import { t as listAgentWorkspaceDirs } from "./workspace-dirs-9oiTy1Sm.js";
import { t as getRemoteSkillEligibility } from "./remote-DoOmoEi1.js";
import { r as updateSkillConfigEntry, t as installSkill } from "./install-CLxRzDc9.js";
import { t as assertValidParams } from "./validation-H8b44ME1.js";
import { createReadStream } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
//#region src/skills/discovery/bins.ts
/** Collects all binary names a set of skills may require or install. */
function collectSkillBins(entries) {
const bins = /* @__PURE__ */ new Set();
for (const entry of entries) {
const required = entry.metadata?.requires?.bins ?? [];
const anyBins = entry.metadata?.requires?.anyBins ?? [];
const install = entry.metadata?.install ?? [];
for (const bin of required) {
const trimmed = bin.trim();
if (trimmed) bins.add(trimmed);
}
for (const bin of anyBins) {
const trimmed = bin.trim();
if (trimmed) bins.add(trimmed);
}
for (const spec of install) {
const specBins = spec?.bins ?? [];
for (const bin of specBins) {
const trimmed = normalizeOptionalString(bin) ?? "";
if (trimmed) bins.add(trimmed);
}
}
}
return [...bins].toSorted();
}
const MAX_SKILL_UPLOAD_BASE64_LENGTH = Math.ceil(4 * 1024 * 1024 / 3) * 4;
const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
const UPLOAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
const locks = /* @__PURE__ */ new Map();
var SkillUploadRequestError = class extends Error {
constructor(message) {
super(message);
this.name = "SkillUploadRequestError";
}
};
async function withLock(key, fn) {
let entry = locks.get(key);
if (!entry) {
entry = {
lock: createAsyncLock(),
references: 0
};
locks.set(key, entry);
}
entry.references += 1;
try {
return await entry.lock(fn);
} finally {
entry.references -= 1;
if (entry.references === 0) locks.delete(key);
}
}
function normalizeSkillUploadSha256(value) {
if (value === void 0) return;
const normalized = value.trim().toLowerCase();
if (!SHA256_PATTERN.test(normalized)) throw new SkillUploadRequestError("invalid sha256");
return normalized;
}
function validateUploadId(uploadId) {
const normalized = uploadId.trim();
if (!UPLOAD_ID_PATTERN.test(normalized)) throw new SkillUploadRequestError("invalid uploadId");
return normalized;
}
function isUploadId(value) {
return UPLOAD_ID_PATTERN.test(value);
}
function validateSizeBytes(sizeBytes) {
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) throw new SkillUploadRequestError("invalid sizeBytes");
if (sizeBytes > 268435456) throw new SkillUploadRequestError("skill archive exceeds maximum upload size");
return sizeBytes;
}
function validateUploadSlug(slug) {
try {
return validateRequestedSkillSlug(slug);
} catch (err) {
throw new SkillUploadRequestError(formatErrorMessage(err));
}
}
function validateOffset(offset) {
if (!Number.isSafeInteger(offset) || offset < 0) throw new SkillUploadRequestError("invalid offset");
return offset;
}
function validateIdempotencyKey(value) {
const normalized = value?.trim();
if (!normalized) return;
if (normalized.length > 2048) throw new SkillUploadRequestError("idempotencyKey is too long");
return normalized;
}
function hashText(value) {
return createHash("sha256").update(value).digest("hex");
}
function resolveUploadsRoot(rootDir) {
return path.resolve(rootDir ?? path.join(resolveStateDir(), "tmp", "skill-uploads"));
}
function resolveUploadDir(rootDir, uploadId) {
return path.join(rootDir, validateUploadId(uploadId));
}
function resolveMetadataPath(rootDir, uploadId) {
return path.join(resolveUploadDir(rootDir, uploadId), "metadata.json");
}
function resolveArchivePath(rootDir, uploadId) {
return path.join(resolveUploadDir(rootDir, uploadId), "archive.zip");
}
function resolveIdempotencyPath(rootDir, keyHash) {
return path.join(rootDir, "idempotency", `${keyHash}.json`);
}
function estimateBase64DecodedBytes(value) {
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
return value.length / 4 * 3 - padding;
}
function decodeBase64Chunk(dataBase64) {
const normalized = dataBase64.trim();
if (!normalized || normalized.length % 4 !== 0 || !BASE64_PATTERN.test(normalized)) throw new SkillUploadRequestError("invalid dataBase64");
if (normalized.length > MAX_SKILL_UPLOAD_BASE64_LENGTH) throw new SkillUploadRequestError("upload chunk exceeds maximum size");
if (estimateBase64DecodedBytes(normalized) > 4194304) throw new SkillUploadRequestError("upload chunk exceeds maximum size");
const decoded = Buffer.from(normalized, "base64");
if (decoded.length < 1) throw new SkillUploadRequestError("empty upload chunk");
if (decoded.length > 4194304) throw new SkillUploadRequestError("upload chunk exceeds maximum size");
return decoded;
}
async function assertNotExpired(rootDir, record, now) {
const validNow = asDateTimestampMs(now);
if (validNow !== void 0 && !isFutureDateTimestampMs(record.expiresAt, { nowMs: validNow })) {
await removeRecordFiles(rootDir, record);
throw new SkillUploadRequestError("upload has expired");
}
if (validNow === void 0) throw new SkillUploadRequestError("upload has expired");
}
async function computeFileSha256(filePath) {
const digest = createHash("sha256");
for await (const chunk of createReadStream(filePath)) digest.update(chunk);
return digest.digest("hex");
}
async function readRecord(rootDir, uploadId) {
const record = await readDurableJsonFile(resolveMetadataPath(rootDir, uploadId));
if (!record || record.version !== 1 || record.uploadId !== uploadId) throw new SkillUploadRequestError(`upload not found: ${uploadId}`);
return {
...record,
archivePath: resolveArchivePath(rootDir, uploadId)
};
}
async function readRecordIfPresent(rootDir, uploadId) {
const record = await readDurableJsonFile(resolveMetadataPath(rootDir, uploadId));
if (!record || record.version !== 1 || record.uploadId !== uploadId) return null;
return {
...record,
archivePath: resolveArchivePath(rootDir, uploadId)
};
}
async function writeRecord(rootDir, record) {
await writeJson(resolveMetadataPath(rootDir, record.uploadId), record, {
mode: 384,
dirMode: 448,
trailingNewline: true
});
}
async function removeUploadDir(rootDir, uploadId) {
await fs$1.rm(resolveUploadDir(rootDir, uploadId), {
recursive: true,
force: true
});
}
async function removeRecordFiles(rootDir, record) {
await removeUploadDir(rootDir, record.uploadId);
if (record.idempotencyKeyHash) await fs$1.rm(resolveIdempotencyPath(rootDir, record.idempotencyKeyHash), { force: true });
}
async function listUploadIds(rootDir) {
return (await fs$1.readdir(rootDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory() && isUploadId(entry.name)).map((entry) => entry.name);
}
async function cleanupExpiredUploads(rootDir, nowMs, excludeUploadId) {
for (const uploadId of await listUploadIds(rootDir)) {
if (uploadId === excludeUploadId) continue;
await withLock(`${rootDir}:upload:${uploadId}`, async () => {
const record = await readRecordIfPresent(rootDir, uploadId).catch(() => null);
const validNow = asDateTimestampMs(nowMs);
if (record && validNow !== void 0 && !isFutureDateTimestampMs(record.expiresAt, { nowMs: validNow })) await removeRecordFiles(rootDir, record);
});
}
}
async function countActiveUploads(rootDir, nowMs) {
let count = 0;
for (const uploadId of await listUploadIds(rootDir)) {
const record = await readRecordIfPresent(rootDir, uploadId).catch(() => null);
if (record && isFutureDateTimestampMs(record.expiresAt, { nowMs })) count += 1;
}
return count;
}
async function writeArchiveChunk(params) {
const handle = await fs$1.open(params.archivePath, "r+");
try {
await handle.truncate(params.offset);
let written = 0;
while (written < params.decoded.length) {
const result = await handle.write(params.decoded, written, params.decoded.length - written, params.offset + written);
if (result.bytesWritten <= 0) throw new Error("failed to write upload chunk");
written += result.bytesWritten;
}
await handle.sync();
await params.afterSync();
} finally {
await handle.close().catch(() => void 0);
}
}
async function readCommittedRecord(rootDir, uploadId, nowMs) {
const record = await readRecord(rootDir, uploadId);
await assertNotExpired(rootDir, record, nowMs);
if (!record.committed) throw new SkillUploadRequestError("upload is not committed");
if (!record.actualSha256) throw new SkillUploadRequestError("committed upload is missing sha256");
const stat = await fs$1.stat(record.archivePath).catch(() => null);
if (!stat || stat.size !== record.sizeBytes) throw new SkillUploadRequestError("uploaded archive is missing or incomplete");
return record;
}
function createSkillUploadStore(options) {
const rootDir = resolveUploadsRoot(options?.rootDir);
const now = options?.now ?? Date.now;
const ttlMs = options?.ttlMs ?? 36e5;
return {
rootDir,
async begin(params) {
return await withLock(`${rootDir}:begin`, async () => {
await cleanupExpiredUploads(rootDir, now());
if (params.kind !== "skill-archive") throw new SkillUploadRequestError("unsupported upload kind");
const slug = validateUploadSlug(params.slug);
const sizeBytes = validateSizeBytes(params.sizeBytes);
const sha256 = normalizeSkillUploadSha256(params.sha256);
const force = params.force === true;
const idempotencyKey = validateIdempotencyKey(params.idempotencyKey);
const keyHash = idempotencyKey ? hashText(idempotencyKey) : void 0;
if (keyHash) {
const existing = await readDurableJsonFile(resolveIdempotencyPath(rootDir, keyHash));
if (existing) {
if (existing.kind !== params.kind || existing.slug !== slug || existing.force !== force || existing.sizeBytes !== sizeBytes || existing.sha256 !== sha256) throw new SkillUploadRequestError("idempotencyKey conflicts with a different upload");
const existingUploadId = validateUploadId(existing.uploadId);
const activeExisting = await withLock(`${rootDir}:upload:${existingUploadId}`, async () => {
const record = await readRecordIfPresent(rootDir, existingUploadId);
if (record && isFutureDateTimestampMs(record.expiresAt, { nowMs: now() })) return {
uploadId: record.uploadId,
receivedBytes: record.receivedBytes,
expiresAt: record.expiresAt
};
if (record) await removeRecordFiles(rootDir, record);
else await removeUploadDir(rootDir, existingUploadId);
return null;
});
if (activeExisting) return activeExisting;
}
}
if (await countActiveUploads(rootDir, now()) >= 32) throw new SkillUploadRequestError("too many active skill uploads");
const uploadId = randomUUID();
const uploadDir = resolveUploadDir(rootDir, uploadId);
const archivePath = resolveArchivePath(rootDir, uploadId);
const createdAt = now();
const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: createdAt });
if (expiresAt === void 0) throw new SkillUploadRequestError("invalid upload expiry");
const record = {
version: 1,
kind: params.kind,
uploadId,
slug,
force,
sizeBytes,
...sha256 ? { sha256 } : {},
receivedBytes: 0,
archivePath,
createdAt,
expiresAt,
committed: false,
...keyHash ? { idempotencyKeyHash: keyHash } : {}
};
await fs$1.mkdir(uploadDir, {
recursive: true,
mode: 448
});
await fs$1.writeFile(archivePath, Buffer.alloc(0), { mode: 384 });
await writeRecord(rootDir, record);
if (keyHash) {
const idem = {
version: 1,
keyHash,
uploadId,
kind: params.kind,
slug,
force,
sizeBytes,
...sha256 ? { sha256 } : {}
};
await writeJson(resolveIdempotencyPath(rootDir, keyHash), idem, {
mode: 384,
dirMode: 448,
trailingNewline: true
});
}
return {
uploadId,
receivedBytes: 0,
expiresAt: record.expiresAt
};
});
},
async chunk(params) {
const uploadId = validateUploadId(params.uploadId);
const offset = validateOffset(params.offset);
const decoded = decodeBase64Chunk(params.dataBase64);
await cleanupExpiredUploads(rootDir, now(), uploadId);
return await withLock(`${rootDir}:upload:${uploadId}`, async () => {
const record = await readRecord(rootDir, uploadId);
await assertNotExpired(rootDir, record, now());
if (record.committed) throw new SkillUploadRequestError("upload is already committed");
if (offset !== record.receivedBytes) throw new SkillUploadRequestError(`upload offset mismatch: expected ${record.receivedBytes}, got ${offset}`);
const nextSize = record.receivedBytes + decoded.length;
if (nextSize > record.sizeBytes) throw new SkillUploadRequestError("upload chunk exceeds declared size");
const nextRecord = {
...record,
receivedBytes: nextSize
};
await writeArchiveChunk({
archivePath: record.archivePath,
offset: record.receivedBytes,
decoded,
afterSync: async () => {
await writeRecord(rootDir, nextRecord);
}
});
return {
uploadId,
receivedBytes: nextRecord.receivedBytes,
expiresAt: nextRecord.expiresAt
};
});
},
async commit(params) {
const uploadId = validateUploadId(params.uploadId);
const requestedSha = normalizeSkillUploadSha256(params.sha256);
return await withLock(`${rootDir}:upload:${uploadId}`, async () => {
const record = await readRecord(rootDir, uploadId);
await assertNotExpired(rootDir, record, now());
if (record.committed) {
if (!record.actualSha256) throw new SkillUploadRequestError("committed upload is missing sha256");
if (requestedSha && requestedSha !== record.actualSha256) throw new SkillUploadRequestError("upload sha256 mismatch");
return {
uploadId,
receivedBytes: record.receivedBytes,
sha256: record.actualSha256,
expiresAt: record.expiresAt
};
}
if (record.receivedBytes !== record.sizeBytes) throw new SkillUploadRequestError(`upload size mismatch: expected ${record.sizeBytes}, got ${record.receivedBytes}`);
const stat = await fs$1.stat(record.archivePath).catch(() => null);
if (!stat || stat.size !== record.sizeBytes) throw new SkillUploadRequestError("uploaded archive is missing or incomplete");
if (record.sha256 && requestedSha && record.sha256 !== requestedSha) throw new SkillUploadRequestError("upload sha256 does not match begin sha256");
const actualSha256 = await computeFileSha256(record.archivePath);
const expectedSha = requestedSha ?? record.sha256;
if (expectedSha && expectedSha !== actualSha256) throw new SkillUploadRequestError("upload sha256 mismatch");
const nextRecord = {
...record,
sha256: record.sha256 ?? requestedSha ?? actualSha256,
actualSha256,
committed: true,
committedAt: now()
};
await writeRecord(rootDir, nextRecord);
return {
uploadId,
receivedBytes: nextRecord.receivedBytes,
sha256: actualSha256,
expiresAt: nextRecord.expiresAt
};
});
},
async withCommittedUpload(uploadIdRaw, action) {
const uploadId = validateUploadId(uploadIdRaw);
return await withLock(`${rootDir}:upload:${uploadId}`, async () => {
const record = await readCommittedRecord(rootDir, uploadId, now());
return await action(record, { remove: async () => {
await removeRecordFiles(rootDir, record);
} });
});
},
async remove(uploadIdRaw) {
const uploadId = validateUploadId(uploadIdRaw);
await withLock(`${rootDir}:upload:${uploadId}`, async () => {
const record = await readDurableJsonFile(resolveMetadataPath(rootDir, uploadId));
if (record && record.version === 1 && record.uploadId === uploadId) await removeRecordFiles(rootDir, record);
else await removeUploadDir(rootDir, uploadId);
});
}
};
}
const defaultSkillUploadStore = createSkillUploadStore();
//#endregion
//#region src/skills/lifecycle/upload-install.ts
/** User-facing disabled message for archive upload installs. */
const UPLOADED_SKILL_ARCHIVES_DISABLED_MESSAGE = "Uploaded skill archive installs are disabled by skills.install.allowUploadedArchives";
function areUploadedSkillArchivesEnabled(config) {
return config.skills?.install?.allowUploadedArchives === true;
}
function uploadInstallFailureErrorKind(failureKind) {
return failureKind === "invalid-request" ? "invalid-request" : "unavailable";
}
async function installUploadedSkillArchive(params) {
const store = params.store ?? defaultSkillUploadStore;
if (!areUploadedSkillArchivesEnabled(params.config)) return {
ok: false,
error: UPLOADED_SKILL_ARCHIVES_DISABLED_MESSAGE,
errorKind: "unavailable"
};
try {
const requestedSlug = validateRequestedSkillSlug(params.slug);
const requestedSha = normalizeSkillUploadSha256(params.sha256);
return await store.withCommittedUpload(params.uploadId, async (record, upload) => {
const rejectInvalid = async (error) => {
await upload.remove().catch(() => void 0);
return {
ok: false,
error,
errorKind: "invalid-request"
};
};
if (record.kind !== "skill-archive") return await rejectInvalid("unsupported upload kind");
if (record.slug !== requestedSlug) return await rejectInvalid("install slug does not match upload slug");
if (record.force !== params.force) return await rejectInvalid("install force does not match upload force");
if (requestedSha && requestedSha !== record.actualSha256) return await rejectInvalid("install sha256 does not match uploaded archive");
if (!record.actualSha256) return await rejectInvalid("committed upload is missing sha256");
const install = await installSkillArchiveFromPath({
archivePath: record.archivePath,
workspaceDir: params.workspaceDir,
slug: record.slug,
force: record.force,
timeoutMs: params.timeoutMs,
logger: params.log,
policy: {
config: params.config,
installId: "upload",
origin: {
type: "upload",
uploadId: params.uploadId,
sha256: record.actualSha256
},
source: {
kind: "upload",
authority: "user",
mutable: false,
network: false
},
requestedSpecifier: `upload:${params.uploadId}`
}
});
if (!install.ok) {
const errorKind = uploadInstallFailureErrorKind(install.failureKind);
if (install.failureKind === "invalid-request") await upload.remove().catch(() => void 0);
return {
ok: false,
error: install.error,
errorKind
};
}
await upload.remove().catch(() => void 0);
return {
ok: true,
message: `Installed ${record.slug}`,
stdout: "",
stderr: "",
code: 0,
slug: record.slug,
targetDir: install.targetDir,
sha256: record.actualSha256
};
});
} catch (err) {
if (err instanceof SkillUploadRequestError) return {
ok: false,
error: err.message,
errorKind: "invalid-request"
};
const error = formatErrorMessage(err);
if (error.startsWith("Invalid skill slug")) return {
ok: false,
error,
errorKind: "invalid-request"
};
return {
ok: false,
error,
errorKind: "unavailable"
};
}
}
//#endregion
//#region src/skills/security/clawhub-verdicts.ts
function readSecurityStatus(security) {
if (!security || typeof security !== "object" || !("status" in security)) return;
const status = security.status;
return typeof status === "string" ? status : void 0;
}
function readSecurityPassed(security) {
if (!security || typeof security !== "object" || !("passed" in security)) return;
const passed = security.passed;
return typeof passed === "boolean" ? passed : void 0;
}
function projectClawHubVerdictItem(item, registry) {
const projected = {
registry,
ok: item.ok,
decision: item.decision,
reasons: item.reasons,
requestedSlug: item.requestedSlug,
requestedVersion: item.requestedVersion
};
if (item.slug !== void 0) projected.slug = item.slug;
if (item.version !== void 0) projected.version = item.version;
if (item.displayName !== void 0) projected.displayName = item.displayName;
if (item.publisherHandle !== void 0) projected.publisherHandle = item.publisherHandle;
if (item.publisherDisplayName !== void 0) projected.publisherDisplayName = item.publisherDisplayName;
if (item.createdAt !== void 0) projected.createdAt = item.createdAt;
if (item.checkedAt !== void 0) projected.checkedAt = item.checkedAt;
if (item.skillUrl !== void 0) projected.skillUrl = item.skillUrl;
if (item.securityAuditUrl !== void 0) projected.securityAuditUrl = item.securityAuditUrl;
const securityStatus = readSecurityStatus(item.security);
if (securityStatus !== void 0) projected.securityStatus = securityStatus;
const securityPassed = readSecurityPassed(item.security);
if (securityPassed !== void 0) projected.securityPassed = securityPassed;
if (item.error) {
const error = {};
if (typeof item.error.code === "string") error.code = item.error.code;
if (typeof item.error.message === "string") error.message = item.error.message;
if (Object.keys(error).length > 0) projected.error = error;
}
return projected;
}
function normalizeAutoVerdictRegistryBase(registry) {
try {
const url = new URL(registry);
const normalizedPath = url.pathname.replace(/\/+$/, "");
return `${url.origin}${normalizedPath}`;
} catch {
return null;
}
}
function canAutoFetchVerdictRegistry(registry) {
const configured = normalizeAutoVerdictRegistryBase(resolveClawHubBaseUrl());
const target = normalizeAutoVerdictRegistryBase(registry);
return configured !== null && target === configured;
}
function collectClawHubVerdictTargets(report) {
const targets = /* @__PURE__ */ new Map();
for (const skill of report.skills) {
const link = skill.clawhub;
if (!link || link.status !== "linked" || !link.valid) continue;
if (!canAutoFetchVerdictRegistry(link.registry)) continue;
const key = `${link.registry}\0${link.slug}\0${link.installedVersion}`;
targets.set(key, {
registry: link.registry,
slug: link.slug,
version: link.installedVersion
});
}
return [...targets.values()];
}
async function fetchOpenClawSkillSecurityVerdicts(targets) {
const byRegistry = /* @__PURE__ */ new Map();
for (const target of targets) {
const registryTargets = byRegistry.get(target.registry) ?? [];
registryTargets.push({
slug: target.slug,
version: target.version
});
byRegistry.set(target.registry, registryTargets);
}
const items = [];
for (const [registry, registryTargets] of byRegistry) {
const response = await fetchClawHubSkillSecurityVerdicts({
baseUrl: registry,
items: registryTargets,
skipAuth: true
});
for (const item of response.items) items.push(projectClawHubVerdictItem(item, registry));
}
return items;
}
//#endregion
//#region src/gateway/server-methods/skills-upload.ts
function uploadErrorShape(prefix, errors) {
return errorShape(ErrorCodes.INVALID_REQUEST, `${prefix}: ${formatValidationErrors(errors)}`);
}
function mapUploadError(err) {
if (err instanceof SkillUploadRequestError) return errorShape(ErrorCodes.INVALID_REQUEST, err.message);
return errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(err));
}
/** Gateway handlers for the staged uploaded-skill archive flow. */
const skillsUploadHandlers = {
"skills.upload.begin": makeUploadHandler("skills.upload.begin", validateSkillsUploadBeginParams, (params) => defaultSkillUploadStore.begin(params)),
"skills.upload.chunk": makeUploadHandler("skills.upload.chunk", validateSkillsUploadChunkParams, (params) => defaultSkillUploadStore.chunk(params)),
"skills.upload.commit": makeUploadHandler("skills.upload.commit", validateSkillsUploadCommitParams, (params) => defaultSkillUploadStore.commit(params))
};
/** Wraps each upload stage with feature gating, protocol validation, and error mapping. */
function makeUploadHandler(name, validator, action) {
return async ({ params, respond, context }) => {
if (!areUploadedSkillArchivesEnabled(context.getRuntimeConfig())) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, UPLOADED_SKILL_ARCHIVES_DISABLED_MESSAGE));
return;
}
if (!validator(params)) {
respond(false, void 0, uploadErrorShape(`invalid ${name} params`, validator.errors));
return;
}
try {
respond(true, await action(params), void 0);
} catch (err) {
respond(false, void 0, mapUploadError(err));
}
};
}
//#endregion
//#region src/gateway/server-methods/skills.ts
function resolveSkillsAgentWorkspace(params, context) {
const cfg = context.getRuntimeConfig();
const agentIdRaw = params && typeof params === "object" && "agentId" in params ? normalizeOptionalString(params.agentId) : void 0;
const agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : resolveDefaultAgentId(cfg);
if (agentIdRaw) {
if (!listAgentIds(cfg).includes(agentId)) return {
ok: false,
error: errorShape(ErrorCodes.INVALID_REQUEST, `unknown agent id "${agentIdRaw}"`)
};
}
return {
ok: true,
cfg,
agentId,
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId)
};
}
function buildRemoteAwareWorkspaceSkillStatus(resolved) {
return buildWorkspaceSkillStatus(resolved.workspaceDir, {
config: resolved.cfg,
eligibility: { remote: getRemoteSkillEligibility({ advertiseExecNode: canExecRequestNode({
cfg: resolved.cfg,
agentId: resolved.agentId
}) }) }
});
}
function respondSkillWorkshopError(respond, err) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, formatErrorMessage(err)));
}
function buildRevisionAgentInstruction(proposal) {
if (!proposal) return "";
return [
`Revise Skill Workshop proposal \`${proposal.record.id}\` (${proposal.record.target.skillKey}).`,
"",
"Use `skill_workshop` with `action=inspect` first, then `action=revise` for that pending proposal.",
"Do not apply, approve, reject, quarantine, or install the proposal.",
"",
"Requested changes:"
].join("\n");
}
const SKILL_PROPOSAL_RESPONSE_HANDLED = Symbol("skill proposal response handled");
async function runSkillsProposalWorkspaceHandler(params) {
if (!assertValidParams(params.rawParams, params.validate, params.method, params.respond)) return;
const resolved = resolveSkillsAgentWorkspace(params.rawParams, params.context);
if (!resolved.ok) {
params.respond(false, void 0, resolved.error);
return;
}
try {
const result = await params.run(params.rawParams, resolved);
if (result !== SKILL_PROPOSAL_RESPONSE_HANDLED) params.respond(true, result, void 0);
} catch (err) {
respondSkillWorkshopError(params.respond, err);
}
}
async function forwardSkillWorkshopRevisionToChatSend(opts, params) {
const { chatHandlers } = await import("./chat-CO9BibHe.js");
const chatSend = chatHandlers["chat.send"];
if (!chatSend) throw new Error("chat.send handler is unavailable");
const chatParams = {
sessionKey: params.sessionKey,
agentId: params.targetAgentId ?? params.agentId,
...params.sessionId ? { sessionId: params.sessionId } : {},
message: params.instructions,
deliver: false,
systemProvenanceReceipt: buildRevisionAgentInstruction(params.proposal),
suppressCommandInterpretation: true,
idempotencyKey: params.idempotencyKey
};
await chatSend({
...opts,
req: {
...opts.req,
method: "chat.send",
params: chatParams
},
params: chatParams
});
}
/** Gateway request handlers for skill status, catalogs, installs, updates, and workshop proposals. */
const skillsHandlers = {
...skillsUploadHandlers,
"skills.status": ({ params, respond, context }) => {
if (!assertValidParams(params, validateSkillsStatusParams, "skills.status", respond)) return;
const resolved = resolveSkillsAgentWorkspace(params, context);
if (!resolved.ok) {
respond(false, void 0, resolved.error);
return;
}
respond(true, buildRemoteAwareWorkspaceSkillStatus(resolved), void 0);
},
"skills.securityVerdicts": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateSkillsSecurityVerdictsParams, "skills.securityVerdicts", respond)) return;
const resolved = resolveSkillsAgentWorkspace(params, context);
if (!resolved.ok) {
respond(false, void 0, resolved.error);
return;
}
try {
const targets = collectClawHubVerdictTargets(buildRemoteAwareWorkspaceSkillStatus(resolved));
if (targets.length === 0) {
respond(true, {
schema: "openclaw.skills.security-verdicts.v1",
items: []
}, void 0);
return;
}
respond(true, {
schema: "openclaw.skills.security-verdicts.v1",
items: await fetchOpenClawSkillSecurityVerdicts(targets)
}, void 0);
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(err)));
}
},
"skills.skillCard": ({ params, respond, context }) => {
if (!assertValidParams(params, validateSkillsSkillCardParams, "skills.skillCard", respond)) return;
const resolved = resolveSkillsAgentWorkspace(params, context);
if (!resolved.ok) {
respond(false, void 0, resolved.error);
return;
}
const skill = buildWorkspaceSkillStatus(resolved.workspaceDir, {
config: resolved.cfg,
agentId: resolved.agentId
}).skills.find((candidate) => candidate.skillKey === params.skillKey);
if (!skill?.skillCard) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `skill card not found for ${params.skillKey}`));
return;
}
const content = readLocalSkillCardContentSync(skill.baseDir);
if (content === void 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `skill card not readable for ${params.skillKey}`));
return;
}
respond(true, {
schema: "openclaw.skills.skill-card.v1",
skillKey: skill.skillKey,
path: skill.skillCard.path,
sizeBytes: skill.skillCard.sizeBytes,
content
}, void 0);
},
"skills.bins": ({ params, respond, context }) => {
if (!assertValidParams(params, validateSkillsBinsParams, "skills.bins", respond)) return;
const cfg = context.getRuntimeConfig();
const workspaceDirs = listAgentWorkspaceDirs(cfg);
const bins = /* @__PURE__ */ new Set();
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkillEntries(workspaceDir, { config: cfg });
for (const bin of collectSkillBins(entries)) bins.add(bin);
}
respond(true, { bins: [...bins].toSorted() }, void 0);
},
"skills.search": async ({ params, respond }) => {
if (!assertValidParams(params, validateSkillsSearchParams, "skills.search", respond)) return;
try {
respond(true, { results: await searchSkillsFromClawHub({
query: params.query,
limit: params.limit
}) }, void 0);
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(err)));
}
},
"skills.detail": async ({ params, respond }) => {
if (!assertValidParams(params, validateSkillsDetailParams, "skills.detail", respond)) return;
try {
respond(true, await fetchClawHubSkillDetail({ slug: params.slug }), void 0);
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(err)));
}
},
"skills.proposals.list": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.list",
rawParams: params,
respond,
context,
validate: validateSkillsProposalsListParams,
run: (_parsedParams, resolved) => listSkillProposals({ workspaceDir: resolved.workspaceDir })
});
},
"skills.proposals.inspect": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.inspect",
rawParams: params,
respond,
context,
validate: validateSkillsProposalInspectParams,
run: async (parsedParams, resolved) => {
const proposal = await inspectSkillProposal(parsedParams.proposalId, { workspaceDir: resolved.workspaceDir });
if (!proposal) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Skill proposal not found: ${parsedParams.proposalId}`));
return SKILL_PROPOSAL_RESPONSE_HANDLED;
}
return proposal;
}
});
},
"skills.proposals.create": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.create",
rawParams: params,
respond,
context,
validate: validateSkillsProposalCreateParams,
run: (parsedParams, resolved) => proposeCreateSkill({
workspaceDir: resolved.workspaceDir,
config: resolved.cfg,
name: parsedParams.name,
description: parsedParams.description,
content: parsedParams.content,
supportFiles: parsedParams.supportFiles,
createdBy: "gateway",
goal: parsedParams.goal,
evidence: parsedParams.evidence
})
});
},
"skills.proposals.update": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.update",
rawParams: params,
respond,
context,
validate: validateSkillsProposalUpdateParams,
run: (parsedParams, resolved) => proposeUpdateSkill({
workspaceDir: resolved.workspaceDir,
config: resolved.cfg,
agentId: resolved.agentId,
skillName: parsedParams.skillName,
description: parsedParams.description,
content: parsedParams.content,
supportFiles: parsedParams.supportFiles,
createdBy: "gateway",
goal: parsedParams.goal,
evidence: parsedParams.evidence
})
});
},
"skills.proposals.revise": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.revise",
rawParams: params,
respond,
context,
validate: validateSkillsProposalReviseParams,
run: (parsedParams, resolved) => reviseSkillProposal({
workspaceDir: resolved.workspaceDir,
config: resolved.cfg,
proposalId: parsedParams.proposalId,
content: parsedParams.content,
supportFiles: parsedParams.supportFiles,
description: parsedParams.description,
goal: parsedParams.goal,
evidence: parsedParams.evidence
})
});
},
"skills.proposals.requestRevision": async (opts) => {
const { params, respond, context } = opts;
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.requestRevision",
rawParams: params,
respond,
context,
validate: validateSkillsProposalRequestRevisionParams,
run: async (parsedParams, resolved) => {
const proposal = await inspectSkillProposal(parsedParams.proposalId, { workspaceDir: resolved.workspaceDir });
if (!proposal) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Skill proposal not found: ${parsedParams.proposalId}`));
return SKILL_PROPOSAL_RESPONSE_HANDLED;
}
if (proposal.record.status !== "pending") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Skill proposal is not pending: ${parsedParams.proposalId}`));
return SKILL_PROPOSAL_RESPONSE_HANDLED;
}
await forwardSkillWorkshopRevisionToChatSend(opts, {
agentId: resolved.agentId,
idempotencyKey: parsedParams.idempotencyKey,
instructions: parsedParams.instructions,
proposal,
sessionId: parsedParams.sessionId,
sessionKey: parsedParams.sessionKey,
targetAgentId: parsedParams.targetAgentId ? normalizeAgentId(parsedParams.targetAgentId) : void 0
});
return SKILL_PROPOSAL_RESPONSE_HANDLED;
}
});
},
"skills.proposals.apply": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.apply",
rawParams: params,
respond,
context,
validate: validateSkillsProposalActionParams,
run: (parsedParams, resolved) => applySkillProposal({
workspaceDir: resolved.workspaceDir,
proposalId: parsedParams.proposalId,
reason: parsedParams.reason
})
});
},
"skills.proposals.reject": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.reject",
rawParams: params,
respond,
context,
validate: validateSkillsProposalActionParams,
run: (parsedParams, resolved) => rejectSkillProposal({
workspaceDir: resolved.workspaceDir,
proposalId: parsedParams.proposalId,
reason: parsedParams.reason
})
});
},
"skills.proposals.quarantine": async ({ params, respond, context }) => {
await runSkillsProposalWorkspaceHandler({
method: "skills.proposals.quarantine",
rawParams: params,
respond,
context,
validate: validateSkillsProposalActionParams,
run: (parsedParams, resolved) => quarantineSkillProposal({
workspaceDir: resolved.workspaceDir,
proposalId: parsedParams.proposalId,
reason: parsedParams.reason
})
});
},
"skills.install": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateSkillsInstallParams, "skills.install", respond)) return;
const cfg = context.getRuntimeConfig();
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
if (params && typeof params === "object" && "source" in params && params.source === "clawhub") {
const p = params;
const result = await installSkillFromClawHub({
workspaceDir: workspaceDirRaw,
slug: p.slug,
version: p.version,
force: Boolean(p.force),
config: cfg
});
respond(result.ok, result.ok ? {
ok: true,
message: `Installed ${result.slug}@${result.version}`,
stdout: "",
stderr: "",
code: 0,
slug: result.slug,
version: result.version,
targetDir: result.targetDir
} : result, result.ok ? void 0 : errorShape(ErrorCodes.UNAVAILABLE, result.error));
return;
}
if (params && typeof params === "object" && "source" in params && params.source === "upload") {
const p = params;
const result = await installUploadedSkillArchive({
uploadId: p.uploadId,
slug: p.slug,
force: Boolean(p.force),
sha256: p.sha256,
timeoutMs: p.timeoutMs,
workspaceDir: workspaceDirRaw,
config: context.getRuntimeConfig(),
log: context.logGateway
});
const errorCode = !result.ok && result.errorKind === "invalid-request" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE;
const responseResult = result.ok ? result : {
ok: false,
error: result.error,
errorCode
};
respond(result.ok, responseResult, result.ok ? void 0 : errorShape(errorCode, result.error));
return;
}
const p = params;
const result = await installSkill({
workspaceDir: workspaceDirRaw,
skillName: p.name,
installId: p.installId,
timeoutMs: p.timeoutMs,
config: cfg
});
respond(result.ok, result, result.ok ? void 0 : errorShape(ErrorCodes.UNAVAILABLE, result.message));
},
"skills.update": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateSkillsUpdateParams, "skills.update", respond)) return;
if (params && typeof params === "object" && "source" in params && params.source === "clawhub") {
const p = params;
if (!p.slug && !p.all) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "clawhub skills.update requires \"slug\" or \"all\""));
return;
}
if (p.slug && p.all) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "clawhub skills.update accepts either \"slug\" or \"all\", not both"));
return;
}
const cfg = context.getRuntimeConfig();
const results = await updateSkillsFromClawHub({
workspaceDir: resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)),
slug: p.slug,
config: cfg
});
const errors = results.filter((result) => !result.ok);
respond(errors.length === 0, {
ok: errors.length === 0,
skillKey: p.slug ?? "*",
config: {
source: "clawhub",
results
}
}, errors.length === 0 ? void 0 : errorShape(ErrorCodes.UNAVAILABLE, errors.map((result) => result.error).join("; ")));
return;
}
const p = params;
const updated = await updateSkillConfigEntry(p);
respond(true, {
ok: true,
skillKey: p.skillKey,
config: redactConfigObject(updated)
}, void 0);
}
};
//#endregion
export { skillsHandlers };