openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,441 lines • 58.5 kB
JavaScript
import "./src-vebZIeLe.js";
import { t as coerceErrorMessage } from "./error-coercion-D_-xJ90S.js";
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js";
import { t as stableStringify } from "./stable-stringify-C8X7niaI.js";
import { At as boolean, Et as array, Nn as record, Rn as string, Tn as object, Xn as union, dn as literal, wn as number, yt as _enum } from "./schemas-zxit8y5H.js";
import { M as assertNoSymlinkParents, t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js";
import { t as parseDurationMs } from "./parse-duration-CuuCHKpt.js";
import { _t as coerceRequiredSqliteNumber, i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { a as expandToolGroups, c as normalizeToolPolicyName, u as resolveToolProfilePolicy } from "./tool-policy-shared-DIyS0iQC.js";
import { o as isToolAllowedByPolicyName } from "./tool-policy-match-TnFxBs5z.js";
import { t as canonicalizeConfiguredMcpServer } from "./mcp-config-normalize-CWrDNtye.js";
import { r as isDangerousHostEnvVarName } from "./host-env-security-BAvlDaJf.js";
import { a as isSupportedLocalAvatarExtension, f as isRenderableAvatarImageDataUrl, u as AVATAR_MAX_DATA_URL_CHARS } from "./avatar-policy-D8FQ_Wte.js";
import { t as MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES } from "./workspace-bootstrap-read-CUL2d6SJ.js";
import { t as computeNextRunAtMs } from "./schedule-Dirub5qQ.js";
import { t as listConfiguredMcpServers } from "./mcp-config-BmNLJlVl.js";
import { a as withClawMcpLifecycleLease, t as setConfiguredMcpServer } from "./mcp-config-mutation-CrkzqS9C.js";
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { realpath, stat } from "node:fs/promises";
import { createHash } from "node:crypto";
import { isScalar, parseDocument, visit } from "yaml";
//#region src/claws/mcp.ts
const CLAW_MCP_REF_SCHEMA_VERSION = "openclaw.clawMcpServerRef.v1";
var ClawMcpInstallError = class extends Error {
constructor(code, message, mcpServers) {
super(message);
this.code = code;
this.mcpServers = mcpServers;
this.name = "ClawMcpInstallError";
}
};
function mcpServerFromActionDetails(details) {
const { expectedState: _expectedState, prerequisites: _prerequisites, ...server } = details;
return "command" in server || "url" in server ? server : void 0;
}
function rowToRef(row) {
return {
schemaVersion: CLAW_MCP_REF_SCHEMA_VERSION,
agentId: row.agent_id,
name: row.name,
configDigest: row.config_digest,
relationship: row.relationship,
origin: row.origin,
independentOwner: coerceRequiredSqliteNumber(row.independent_owner) === 1,
status: row.status,
...row.error ? { error: row.error } : {},
createdAtMs: coerceRequiredSqliteNumber(row.created_at_ms),
updatedAtMs: coerceRequiredSqliteNumber(row.updated_at_ms)
};
}
function digestClawMcpServer(server) {
const canonical = canonicalizeConfiguredMcpServer(server);
return `sha256:${createHash("sha256").update(stableStringify(canonical)).digest("hex")}`;
}
function persistPendingRef(plan, name, server, ownership, options) {
const nowMs = options.nowMs ?? Date.now();
const configDigest = digestClawMcpServer(server);
const existing = openOpenClawStateDatabase(options).db.prepare(`SELECT schema_version, agent_id, name, config_digest, relationship, origin,
independent_owner, status, error,
created_at_ms, updated_at_ms
FROM claw_mcp_server_refs
WHERE agent_id = ? AND name = ?`).get(plan.agent.finalId, name);
if (existing) {
const ref = rowToRef(existing);
if (ref.configDigest !== configDigest || ref.status === "failed") throw new ClawMcpInstallError("mcp_provenance_conflict", `MCP server ${JSON.stringify(name)} differs from its ownership record.`, [ref]);
return {
ref,
existing: true
};
}
const ref = {
schemaVersion: CLAW_MCP_REF_SCHEMA_VERSION,
agentId: plan.agent.finalId,
name,
configDigest,
...ownership,
status: "pending",
createdAtMs: nowMs,
updatedAtMs: nowMs
};
runOpenClawStateWriteTransaction(({ db }) => {
db.prepare(`INSERT INTO claw_mcp_server_refs (
agent_id, name, schema_version, config_digest, relationship, origin,
independent_owner, status, error,
created_at_ms, updated_at_ms
) VALUES (
@agent_id, @name, @schema_version, @config_digest, @relationship, @origin,
@independent_owner, @status, NULL,
@created_at_ms, @updated_at_ms
)`).run({
agent_id: ref.agentId,
name: ref.name,
schema_version: ref.schemaVersion,
config_digest: ref.configDigest,
relationship: ref.relationship,
origin: ref.origin,
independent_owner: ref.independentOwner ? 1 : 0,
status: ref.status,
created_at_ms: nowMs,
updated_at_ms: nowMs
});
}, options);
return {
ref,
existing: false
};
}
function updateRef(ref, update, options) {
const updated = {
...ref,
...update,
updatedAtMs: options.nowMs ?? Date.now()
};
runOpenClawStateWriteTransaction(({ db }) => {
db.prepare(`UPDATE claw_mcp_server_refs
SET status = @status, error = @error, updated_at_ms = @updated_at_ms
WHERE agent_id = @agent_id AND name = @name`).run({
agent_id: ref.agentId,
name: ref.name,
status: update.status,
error: update.error ?? null,
updated_at_ms: updated.updatedAtMs
});
}, options);
return updated;
}
async function installClawMcpServers(plan, options = {}) {
const setMcpServer = options.setMcpServer ?? setConfiguredMcpServer;
const listMcpServers = options.listMcpServers ?? listConfiguredMcpServers;
const refs = [];
for (const action of plan.actions.filter((candidate) => candidate.kind === "mcpServer")) await withClawMcpLifecycleLease(action.id, options, async () => {
const server = action.details ? mcpServerFromActionDetails(action.details) : void 0;
if (!server) throw new ClawMcpInstallError("mcp_plan_invalid", `MCP server action ${JSON.stringify(action.id)} is invalid.`, refs);
const listed = await listMcpServers();
if (!listed.ok) throw new ClawMcpInstallError("mcp_preflight_failed", listed.error, refs);
const configured = listed.mcpServers[action.id];
const configDigest = digestClawMcpServer(server);
if (configured && digestClawMcpServer(configured) !== configDigest) throw new ClawMcpInstallError("mcp_config_conflict", `MCP server ${JSON.stringify(action.id)} already exists with different configuration.`, refs);
const existingRefs = readClawMcpServerRefsByName(action.id, options);
const inheritsClawOrigin = existingRefs.length > 0 && existingRefs.every((candidate) => candidate.origin === "claw-introduced" && !candidate.independentOwner);
const ownership = configured ? {
relationship: "referenced",
origin: inheritsClawOrigin ? "claw-introduced" : "pre-existing",
independentOwner: !inheritsClawOrigin
} : {
relationship: "managed",
origin: "claw-introduced",
independentOwner: false
};
const pendingResult = persistPendingRef(plan, action.id, server, ownership, options);
let pending = pendingResult.ref;
refs.push(pending);
if (pending.status === "complete") {
if (configured) return;
const hasSiblingOwner = readClawMcpServerRefsByName(action.id, options).some((candidate) => candidate.agentId !== plan.agent.finalId);
if (pending.relationship !== "managed" || pending.origin !== "claw-introduced" || pending.independentOwner || hasSiblingOwner) throw new ClawMcpInstallError("mcp_reconcile_conflict", `MCP server ${JSON.stringify(action.id)} was removed while shared or independently owned and will not be recreated.`, refs);
pending = updateRef(pending, { status: "pending" }, options);
refs[refs.length - 1] = pending;
}
if (pendingResult.existing && configured) {
if (digestClawMcpServer(configured) !== pending.configDigest) throw new ClawMcpInstallError("mcp_reconcile_conflict", `MCP server ${JSON.stringify(action.id)} changed after an ambiguous write.`, refs);
refs[refs.length - 1] = updateRef(pending, { status: "complete" }, options);
return;
}
if (configured) {
refs[refs.length - 1] = updateRef(pending, { status: "complete" }, options);
return;
}
let result;
try {
result = await setMcpServer({
name: action.id,
server,
createOnly: true,
recordIndependentOwner: false
});
} catch (error) {
throw new ClawMcpInstallError("mcp_install_uncertain", coerceErrorMessage(error), refs);
}
if (!result.ok) {
refs[refs.length - 1] = updateRef(pending, {
status: "failed",
error: result.error
}, options);
throw new ClawMcpInstallError("mcp_install_failed", result.error, refs);
}
try {
refs[refs.length - 1] = updateRef(pending, { status: "complete" }, options);
} catch (error) {
throw new ClawMcpInstallError("mcp_provenance_failed", `MCP server was configured, but ownership could not be persisted: ${coerceErrorMessage(error)}`, refs);
}
});
return refs;
}
function readClawMcpServerRefs(agentId, options = {}) {
const database = openOpenClawStateDatabase(options);
if (options.readOnly && !database.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'claw_mcp_server_refs'").get()) return [];
return database.db.prepare(`SELECT schema_version, agent_id, name, config_digest, relationship, origin,
independent_owner, status, error,
created_at_ms, updated_at_ms
FROM claw_mcp_server_refs
WHERE agent_id = ?
ORDER BY name`).all(agentId).map(rowToRef);
}
function readClawMcpServerRefsByName(name, options = {}) {
const database = openOpenClawStateDatabase(options);
if (options.readOnly && !database.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'claw_mcp_server_refs'").get()) return [];
return database.db.prepare(`SELECT schema_version, agent_id, name, config_digest, relationship, origin,
independent_owner, status, error,
created_at_ms, updated_at_ms
FROM claw_mcp_server_refs
WHERE name = ?
ORDER BY agent_id`).all(name).map(rowToRef);
}
function clawMcpRemovalSelector(ref) {
return `mcp:${ref.name}`;
}
function planClawMcpServerRemoval(ref, options = {}) {
const affectedClawAgentIds = readClawMcpServerRefsByName(ref.name, options).filter((candidate) => candidate.agentId !== ref.agentId).map((candidate) => candidate.agentId).toSorted();
const cleanup = options.referencedCleanup ?? { mode: "retain" };
const explicitlySelected = cleanup.mode === "remove-selected" && (cleanup.selected ?? []).includes(clawMcpRemovalSelector(ref));
const conflicts = affectedClawAgentIds.length > 0 || ref.independentOwner || ref.origin === "pre-existing";
const release = (reason, blocked = false) => ({
ref,
action: "release",
blocked,
affectedClawAgentIds,
reason
});
if (ref.relationship === "managed") {
if (explicitlySelected) return release("--remove-referenced only accepts resources with a referenced relationship.", true);
if (affectedClawAgentIds.length > 0) return release("Another Claw still references this MCP server.");
if (ref.independentOwner) return release("MCP server has a current non-Claw owner.");
return {
ref,
action: "remove",
blocked: false,
affectedClawAgentIds
};
}
if (!explicitlySelected && cleanup.mode !== "remove-if-unused") return release("Referenced resources are retained unless a cleanup mode selects them.");
if (!explicitlySelected && conflicts) return release(affectedClawAgentIds.length > 0 ? "Another Claw still references this MCP server." : "MCP server has a current non-Claw owner or pre-existing origin.");
if (explicitlySelected && conflicts && !cleanup.allowConflicts) return release("Selected MCP server has other Claw dependents, a non-Claw owner, or pre-existing origin; explicit conflict override is required.", true);
return {
ref,
action: "remove",
blocked: false,
affectedClawAgentIds
};
}
function reconcileClawMcpServerRefs(agentId, configuredServers, options = {}) {
return readClawMcpServerRefs(agentId, options).map((ref) => {
if (ref.status !== "pending") return ref;
const configured = configuredServers[ref.name];
return configured && digestClawMcpServer(configured) === ref.configDigest ? updateRef(ref, { status: "complete" }, options) : ref;
});
}
function deleteClawMcpServerRef(agentId, name, options = {}) {
runOpenClawStateWriteTransaction(({ db }) => {
db.prepare("DELETE FROM claw_mcp_server_refs WHERE agent_id = ? AND name = ?").run(agentId, name);
}, options);
}
function upsertClawMcpServerRef(ref, options = {}) {
runOpenClawStateWriteTransaction(({ db }) => {
db.prepare(`INSERT INTO claw_mcp_server_refs (
agent_id, name, schema_version, config_digest, relationship, origin,
independent_owner, status, error,
created_at_ms, updated_at_ms
) VALUES (
@agent_id, @name, @schema_version, @config_digest, @relationship, @origin,
@independent_owner, @status, @error,
@created_at_ms, @updated_at_ms
)
ON CONFLICT(agent_id, name) DO UPDATE SET
schema_version = excluded.schema_version,
config_digest = excluded.config_digest,
relationship = excluded.relationship,
origin = excluded.origin,
independent_owner = excluded.independent_owner,
status = excluded.status,
error = excluded.error,
updated_at_ms = excluded.updated_at_ms`).run({
agent_id: ref.agentId,
name: ref.name,
schema_version: ref.schemaVersion,
config_digest: ref.configDigest,
relationship: ref.relationship,
origin: ref.origin,
independent_owner: ref.independentOwner ? 1 : 0,
status: ref.status,
error: ref.error ?? null,
created_at_ms: ref.createdAtMs,
updated_at_ms: ref.updatedAtMs
});
}, options);
}
//#endregion
//#region src/claws/types.ts
const CLAW_ADD_PLAN_SCHEMA_VERSION = "openclaw.clawAddPlan.v1";
const CLAW_INSPECT_RESULT_SCHEMA_VERSION = "openclaw.clawInspect.v1";
const CLAW_OUTPUT_STABILITY = "experimental";
const CLAW_BOOTSTRAP_FILE_NAMES = [
"AGENTS.md",
"SOUL.md",
"IDENTITY.md",
"TOOLS.md",
"HEARTBEAT.md"
];
//#endregion
//#region src/claws/schema-portability.ts
const EXACT_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
const WINDOWS_INVALID_PATH_CHARS = /[<>:"|?*]/;
const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
const BASE64_PAYLOAD_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
function isExactSemVer(value) {
return EXACT_VERSION_PATTERN.test(value);
}
function isCanonicalClawHubPackageName(value) {
return PACKAGE_NAME_PATTERN.test(value);
}
function isSafeClawRelativePath(value) {
const normalized = value.replaceAll("\\", "/");
if (normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) return false;
return normalized.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && !WINDOWS_INVALID_PATH_CHARS.test(segment) && !Array.from(segment).some((character) => character.charCodeAt(0) <= 31) && !segment.endsWith(".") && !segment.endsWith(" ") && !WINDOWS_RESERVED_PATH_SEGMENT.test(segment));
}
function portableClawPathKey(value) {
return value.replaceAll("\\", "/").normalize("NFC").toLowerCase();
}
function conflictsWithClawPath(targets, candidate) {
for (const target of targets) if (target === candidate || target.startsWith(`${candidate}/`) || candidate.startsWith(`${target}/`)) return true;
return false;
}
function isPortableClawAvatar(value) {
if (isRenderableAvatarImageDataUrl(value)) {
if (value.length > AVATAR_MAX_DATA_URL_CHARS) return false;
const comma = value.indexOf(",");
if (comma < 0) return false;
const metadata = value.slice(0, comma);
const payload = value.slice(comma + 1);
try {
const base64 = /;base64(?:;|$)/i.test(metadata);
if (payload.length === 0 || base64 && !BASE64_PAYLOAD_PATTERN.test(payload)) return false;
const bytes = base64 ? Buffer.from(payload, "base64") : Buffer.from(decodeURIComponent(payload), "utf8");
return bytes.byteLength > 0 && bytes.byteLength <= 2097152;
} catch {
return false;
}
}
return isSafeClawRelativePath(value) && isSupportedLocalAvatarExtension(value);
}
function isValidClawTimezone(value) {
try {
new Intl.DateTimeFormat("en-US", { timeZone: value }).format();
return true;
} catch {
return false;
}
}
function packageManagerArtifacts(command, args) {
const executable = command.split(/[\\/]/).at(-1)?.replace(/\.(?:cmd|exe)$/i, "").toLowerCase();
let start = 0;
if (executable === "pnpm" || executable === "yarn") {
if (args[0] !== "dlx") return;
start = 1;
} else if (executable !== "npx" && executable !== "pnpx" && executable !== "bunx") return;
const selected = [];
let positional;
for (let index = start; index < args.length; index += 1) {
const value = args[index];
if (!value) continue;
if (value === "--") {
positional = args[index + 1] ?? "";
break;
}
if (value === "-p" || value === "--package") {
selected.push(args[index + 1] ?? "");
index += 1;
continue;
}
if (value.startsWith("--package=")) {
selected.push(value.slice(10));
continue;
}
if (positional === void 0 && !value.startsWith("-")) {
positional = value;
break;
}
}
return selected.length > 0 ? selected : [positional ?? ""];
}
function isClawPackageManagerArtifactPinned(command, args) {
const artifacts = packageManagerArtifacts(command, args);
if (artifacts === void 0) return;
return artifacts.every((artifact) => {
const separator = artifact.lastIndexOf("@");
const scopedSlash = artifact.startsWith("@") ? artifact.indexOf("/") : -1;
return separator > 0 && separator > scopedSlash && isExactSemVer(artifact.slice(separator + 1));
});
}
//#endregion
//#region src/claws/tool-profile-consent.ts
function isConcreteBundleMcpToolName(name) {
return name.length <= 64 && /^[A-Za-z][A-Za-z0-9_-]*__[A-Za-z][A-Za-z0-9_-]*$/u.test(name);
}
function resolveClawToolProfileSnapshot(tools) {
if (!tools.profile) return;
const profile = resolveToolProfilePolicy(tools.profile);
if (!profile) return;
const profileAllow = expandToolGroups(profile.allow);
return {
allow: (tools.allow ? profileAllow.includes("*") ? expandToolGroups(tools.allow) : Array.from(/* @__PURE__ */ new Set([...profileAllow.filter((tool) => isToolAllowedByPolicyName(tool, { allow: tools.allow })), ...profileAllow.includes("bundle-mcp") ? tools.allow.filter(isConcreteBundleMcpToolName) : []])) : void 0) ?? expandToolGroups([...profile.allow ?? [], ...tools.alsoAllow ?? []]),
deny: expandToolGroups([...profile.deny ?? [], ...tools.deny ?? []])
};
}
function materializeClawToolProfile(settings, options = {}) {
const tools = settings.tools;
if (!tools) return settings;
if (!tools.profile) {
const allow = expandToolGroups(tools.allow);
const deny = expandToolGroups(tools.deny);
return {
...settings,
tools: {
...allow.length > 0 ? {
profile: "full",
allow
} : {},
...tools.alsoAllow ? { alsoAllow: expandToolGroups(tools.alsoAllow) } : {},
...deny.length > 0 ? { deny } : {},
...tools.fs ? { fs: tools.fs } : {}
}
};
}
const snapshot = resolveClawToolProfileSnapshot(tools);
if (!snapshot) return settings;
if (tools.profile === "full" && !tools.allow) throw new Error("Claw full tool profile requires a bounded explicit allowlist.");
if (tools.allow && snapshot.allow.length === 0) throw new Error("Claw tool allowlist does not overlap the selected profile.");
const allow = options.allowLegacyDynamicProfile ? snapshot.allow.filter((grant) => grant !== "bundle-mcp") : snapshot.allow;
if (allow.includes("bundle-mcp")) throw new Error("Claw tool profiles containing bundle-mcp require an explicit bounded allowlist of concrete tool names.");
if (allow.length === 0) throw new Error("Legacy Claw tool profile has no bounded authority to preserve.");
return {
...settings,
tools: {
profile: "full",
allow,
...snapshot.deny.length > 0 ? { deny: snapshot.deny } : {},
...tools.fs ? { fs: tools.fs } : {}
}
};
}
//#endregion
//#region src/claws/schema.ts
const nonEmptyString = string().min(1).refine((value) => value.length === value.trim().length && value.length > 0, "Value must not have leading or trailing whitespace.");
const optionalString = nonEmptyString.optional();
function isBoundedClawToolGrant(value) {
const normalized = normalizeToolPolicyName(value);
if (/[*?[\]{}]/u.test(normalized) || normalized === "group:plugins" || normalized === "bundle-mcp") return false;
return !normalized.startsWith("group:") || expandToolGroups([normalized])[0] !== normalized;
}
function clawManifestWorkspaceConflictsWithPath(manifest, path) {
const targets = /* @__PURE__ */ new Set();
for (const name of CLAW_BOOTSTRAP_FILE_NAMES) if (manifest.workspace.bootstrapFiles[name]) targets.add(portableClawPathKey(name));
for (const file of manifest.workspace.files) targets.add(portableClawPathKey(file.path));
return conflictsWithClawPath(targets, portableClawPathKey(path));
}
const agentId = nonEmptyString.regex(/^[a-z][a-z0-9_-]{0,63}$/, "Agent id must start with a lowercase letter and contain only lowercase letters, digits, underscores, or hyphens.");
const exactVersion = nonEmptyString.refine(isExactSemVer, "Package version must be an exact semantic version.");
const clawHubPackageName = nonEmptyString.refine(isCanonicalClawHubPackageName, "ClawHub package references must use their canonical lowercase name.");
const portableEnvKey = /^[A-Za-z_][A-Za-z0-9_]*$/;
const packageRelativePath = nonEmptyString.refine(isSafeClawRelativePath, { message: "Path must be package-relative and must not contain traversal segments." }).transform((value) => value.replaceAll("\\", "/"));
const identitySchema = object({
name: optionalString,
theme: optionalString,
emoji: optionalString,
avatar: nonEmptyString.refine(isPortableClawAvatar, { message: "Avatar must be a bounded image data URL or managed workspace-relative image path." }).optional()
}).strict();
const agentSchema = object({
id: agentId,
name: optionalString,
description: optionalString,
identity: identitySchema.optional()
}).strict();
const openClawExtensionSchema = object({
id: agentId,
kind: literal("plugin"),
format: _enum([
"openclaw",
"claude",
"codex",
"cursor"
]),
source: literal("clawhub"),
ref: clawHubPackageName,
version: exactVersion
}).strict();
const openClawProfileSchema = object({
schemaVersion: literal(1),
agent: object({
groupChat: object({ mentionPatterns: array(nonEmptyString).min(1).optional() }).strict().optional(),
sandbox: object({
mode: _enum([
"off",
"non-main",
"all"
]).optional(),
scope: _enum([
"session",
"agent",
"shared"
]).optional(),
workspaceAccess: _enum([
"none",
"ro",
"rw"
]).optional()
}).strict().optional(),
tools: object({
profile: nonEmptyString.refine((value) => resolveToolProfilePolicy(value) !== void 0, "Tool profile must name a registered OpenClaw built-in profile.").optional(),
allow: array(nonEmptyString.refine(isBoundedClawToolGrant, "Tool grants must be bounded.")).min(1).optional(),
alsoAllow: array(nonEmptyString.refine(isBoundedClawToolGrant, "Tool grants must be bounded.")).min(1).optional(),
deny: array(nonEmptyString).min(1).optional(),
fs: object({ workspaceOnly: literal(true).optional() }).strict().optional()
}).strict().superRefine((tools, ctx) => {
if (tools.profile === "full" && !tools.allow) ctx.addIssue({
code: "custom",
path: ["profile"],
message: "The full tool profile requires a bounded explicit allowlist."
});
if (tools.profile && tools.profile !== "full" && tools.allow) {
const profileAllow = expandToolGroups(resolveToolProfilePolicy(tools.profile)?.allow);
if (tools.allow.some((grant) => !profileAllow.some((tool) => isToolAllowedByPolicyName(tool, { allow: [grant] })) && !(profileAllow.includes("bundle-mcp") && isConcreteBundleMcpToolName(grant)))) ctx.addIssue({
code: "custom",
path: ["allow"],
message: "Every agent tools allow grant must overlap the selected profile."
});
}
if (tools.profile && resolveClawToolProfileSnapshot(tools)?.allow.includes("bundle-mcp")) ctx.addIssue({
code: "custom",
path: ["allow"],
message: "Profiles containing bundle-mcp require a bounded allowlist of concrete tool names."
});
if (tools.alsoAllow && !tools.profile) ctx.addIssue({
code: "custom",
path: ["alsoAllow"],
message: "Agent tools can set alsoAllow only when a bounded profile is selected."
});
if (tools.allow && tools.alsoAllow) ctx.addIssue({
code: "custom",
path: ["alsoAllow"],
message: "Agent tools cannot set both allow and alsoAllow; use allow alone or profile with alsoAllow."
});
}).optional(),
memory: object({ search: object({
enabled: boolean().optional(),
rememberAcrossConversations: boolean().optional(),
sources: array(_enum(["memory", "sessions"])).min(1).optional()
}).strict().superRefine((search, ctx) => {
if (search.sources?.includes("sessions") && search.rememberAcrossConversations !== true) ctx.addIssue({
code: "custom",
path: ["rememberAcrossConversations"],
message: "The sessions source requires rememberAcrossConversations: true in the OpenClaw profile."
});
}).optional() }).strict().optional(),
heartbeat: object({
every: nonEmptyString.refine((value) => {
try {
parseDurationMs(value, { defaultUnit: "m" });
return true;
} catch {
return false;
}
}, "Invalid heartbeat duration.").optional(),
activeHours: object({
start: nonEmptyString.regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/).optional(),
end: nonEmptyString.regex(/^(?:(?:[01]\d|2[0-3]):[0-5]\d|24:00)$/).optional(),
timezone: nonEmptyString.refine(isValidClawTimezone, "Invalid IANA timezone.").optional()
}).strict().optional(),
lightContext: boolean().optional(),
isolatedSession: boolean().optional(),
timeoutSeconds: number().int().positive().optional()
}).strict().optional(),
humanDelay: object({
mode: _enum([
"off",
"natural",
"custom"
]).optional(),
minMs: number().int().nonnegative().optional(),
maxMs: number().int().nonnegative().optional()
}).strict().optional()
}).strict(),
extensions: array(openClawExtensionSchema).optional().default([])
}).strict().superRefine((profile, ctx) => {
const ids = /* @__PURE__ */ new Set();
const refs = /* @__PURE__ */ new Set();
profile.extensions.forEach((extension, index) => {
if (ids.has(extension.id)) ctx.addIssue({
code: "custom",
path: [
"extensions",
index,
"id"
],
message: `Extension id ${JSON.stringify(extension.id)} is declared more than once.`
});
ids.add(extension.id);
const ref = extension.ref.toLowerCase();
if (refs.has(ref)) ctx.addIssue({
code: "custom",
path: [
"extensions",
index,
"ref"
],
message: `Extension ${JSON.stringify(extension.ref)} is declared more than once.`
});
refs.add(ref);
});
});
const workspaceSourceSchema = object({ source: packageRelativePath }).strict();
const bootstrapFilesSchema = object(Object.fromEntries(CLAW_BOOTSTRAP_FILE_NAMES.map((name) => [name, workspaceSourceSchema.optional()]))).partial().strict();
const workspaceFileSchema = object({
source: packageRelativePath,
path: packageRelativePath
}).strict();
const workspaceSchema = object({
bootstrapFiles: bootstrapFilesSchema.optional().default({}),
files: array(workspaceFileSchema).optional().default([])
}).strict().default({
bootstrapFiles: {},
files: []
});
const packageSchema = object({
kind: _enum(["skill", "plugin"]),
source: literal("clawhub"),
ref: clawHubPackageName,
version: exactVersion
}).strict();
const environmentReference = nonEmptyString.regex(/^\$\{[A-Z_][A-Z0-9_]*\}$/, "MCP environment values must be unresolved ${ENV_VAR} references.");
const mcpServerCommonShape = {
toolFilter: object({
include: array(nonEmptyString).min(1).optional(),
exclude: array(nonEmptyString).min(1).optional()
}).strict().superRefine((filter, ctx) => {
for (const field of ["include", "exclude"]) {
const seen = /* @__PURE__ */ new Set();
for (const [index, value] of (filter[field] ?? []).entries()) {
if (value.includes("?") || value.includes("[") || value.includes("]")) ctx.addIssue({
code: "custom",
path: [field, index],
message: "Tool filters support only exact names and * wildcards."
});
if (seen.has(value)) ctx.addIssue({
code: "custom",
path: [field, index],
message: "Tool filter entries must be unique."
});
seen.add(value);
}
}
}).optional(),
timeout: number().finite().positive().optional(),
connectTimeout: number().finite().positive().optional()
};
const stdioMcpServerSchema = object({
command: nonEmptyString,
transport: literal("stdio").optional(),
args: array(nonEmptyString).optional(),
env: record(nonEmptyString.regex(portableEnvKey, "Invalid portable environment key."), environmentReference).optional(),
...mcpServerCommonShape
}).strict().superRefine((server, ctx) => {
if (isClawPackageManagerArtifactPinned(server.command, server.args ?? []) === false) ctx.addIssue({
code: "custom",
path: ["args"],
message: "Package-manager MCP commands must select one exact immutable package version."
});
for (const key of Object.keys(server.env ?? {})) if (isDangerousHostEnvVarName(key)) ctx.addIssue({
code: "custom",
path: ["env", key],
message: "Environment key is blocked by the spawned-process safety policy."
});
});
const remoteMcpServerSchema = object({
url: nonEmptyString.url(),
transport: _enum(["sse", "streamable-http"]),
auth: literal("oauth").optional(),
...mcpServerCommonShape
}).strict().superRefine((server, ctx) => {
const url = new URL(server.url);
const loopback = [
"localhost",
"127.0.0.1",
"[::1]"
].includes(url.hostname);
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ctx.addIssue({
code: "custom",
path: ["url"],
message: "Remote MCP URLs must use HTTPS, except HTTP on an exact loopback host."
});
if (url.username || url.password || url.hash) ctx.addIssue({
code: "custom",
path: ["url"],
message: "Remote MCP URLs must not contain user information or fragments."
});
});
const mcpServerSchema = union([stdioMcpServerSchema, remoteMcpServerSchema]);
const cronJobSchema = object({
id: agentId,
name: optionalString,
schedule: object({
cron: nonEmptyString,
timezone: nonEmptyString
}).strict(),
session: _enum(["main", "isolated"]),
message: nonEmptyString,
delivery: object({
mode: _enum(["none", "announce"]),
channel: literal("last").optional()
}).strict().optional()
}).strict().superRefine((job, ctx) => {
if (job.schedule.cron.trim().split(/\s+/).length !== 5) ctx.addIssue({
code: "custom",
path: ["schedule", "cron"],
message: "Cron schedule must use exactly five fields."
});
if (job.delivery?.mode === "none" && job.delivery.channel !== void 0 || job.delivery?.mode === "announce" && job.delivery.channel !== "last") ctx.addIssue({
code: "custom",
path: ["delivery"],
message: "Delivery must be { mode: \"none\" } or { mode: \"announce\", channel: \"last\" }."
});
try {
computeNextRunAtMs({
kind: "cron",
expr: job.schedule.cron,
tz: job.schedule.timezone
}, Date.now());
} catch {
ctx.addIssue({
code: "custom",
path: ["schedule", "cron"],
message: "Invalid cron expression or timezone."
});
}
});
const manifestSchema = object({
schemaVersion: literal(1),
agent: agentSchema,
metadata: record(nonEmptyString, string()).optional().default({}),
workspace: workspaceSchema.optional().default({
bootstrapFiles: {},
files: []
}),
packages: array(packageSchema).optional().default([]),
mcpServers: record(nonEmptyString.regex(/^[a-z][a-z0-9_-]{0,63}$/, "Invalid MCP server name."), mcpServerSchema).optional().default({}),
cronJobs: array(cronJobSchema).optional().default([])
}).strict().superRefine((manifest, ctx) => {
const workspaceTargets = /* @__PURE__ */ new Set();
const nativeBootstrapTarget = /* @__PURE__ */ new Set([portableClawPathKey("BOOTSTRAP.md")]);
for (const name of CLAW_BOOTSTRAP_FILE_NAMES) if (manifest.workspace.bootstrapFiles[name]) workspaceTargets.add(portableClawPathKey(name));
manifest.workspace.files.forEach((file, index) => {
const destinationKey = portableClawPathKey(file.path);
if (conflictsWithClawPath(nativeBootstrapTarget, destinationKey)) ctx.addIssue({
code: "custom",
path: [
"workspace",
"files",
index,
"path"
],
message: "Package-root BOOTSTRAP.md uses the native seed-once lifecycle and cannot be a managed workspace destination."
});
if (conflictsWithClawPath(workspaceTargets, destinationKey)) ctx.addIssue({
code: "custom",
path: [
"workspace",
"files",
index,
"path"
],
message: `Workspace destination ${JSON.stringify(file.path)} is declared more than once.`
});
workspaceTargets.add(destinationKey);
});
const packageKeys = /* @__PURE__ */ new Set();
manifest.packages.forEach((pkg, index) => {
const key = `${pkg.kind}:${pkg.source}:${pkg.ref.toLowerCase()}`;
if (packageKeys.has(key)) ctx.addIssue({
code: "custom",
path: ["packages", index],
message: `Package ${JSON.stringify(pkg.ref)} is declared more than once for ${pkg.kind}.`
});
packageKeys.add(key);
});
const managedPaths = new Set(manifest.workspace.files.map((file) => portableClawPathKey(file.path)));
const avatar = manifest.agent.identity?.avatar;
if (avatar && !isRenderableAvatarImageDataUrl(avatar) && !managedPaths.has(portableClawPathKey(avatar))) ctx.addIssue({
code: "custom",
path: [
"agent",
"identity",
"avatar"
],
message: "Workspace-relative avatar must match a workspace.files destination."
});
const cronIds = /* @__PURE__ */ new Set();
manifest.cronJobs.forEach((job, index) => {
if (cronIds.has(job.id)) ctx.addIssue({
code: "custom",
path: [
"cronJobs",
index,
"id"
],
message: `Cron job id ${JSON.stringify(job.id)} is declared more than once.`
});
cronIds.add(job.id);
});
});
function formatIssuePath(path) {
if (path.length === 0) return "$";
return `$${path.map((part) => typeof part === "number" ? `[${part}]` : `.${String(part)}`).join("")}`;
}
function diagnosticsFromZodError(error) {
return error.issues.map((issue) => ({
level: "error",
code: "invalid_manifest",
phase: "schema",
path: formatIssuePath(issue.path),
message: issue.message
}));
}
function parseClawManifest(value) {
const parsed = manifestSchema.safeParse(value);
if (!parsed.success) return {
ok: false,
diagnostics: diagnosticsFromZodError(parsed.error)
};
return {
ok: true,
manifest: parsed.data,
diagnostics: []
};
}
function parseClawOpenClawProfile(value) {
const parsed = openClawProfileSchema.safeParse(value);
if (!parsed.success) return {
ok: false,
diagnostics: diagnosticsFromZodError(parsed.error)
};
return {
ok: true,
profile: parsed.data,
diagnostics: []
};
}
//#endregion
//#region src/claws/yaml-document.ts
function diagnostic$1(code, message) {
return {
level: "error",
code,
phase: "parse",
path: "$",
message
};
}
function parseClawYaml(raw, path, kind) {
const [invalidCode, unsupportedCode, label] = kind === "frontmatter" ? [
"invalid_claw_frontmatter",
"unsupported_claw_yaml_feature",
"CLAW.md frontmatter"
] : [
"invalid_openclaw_profile",
"unsupported_openclaw_profile_yaml_feature",
"OpenClaw profile YAML"
];
const document = parseDocument(raw, {
prettyErrors: false,
uniqueKeys: true
});
if (document.errors.length > 0) return {
ok: false,
diagnostics: document.errors.map((error) => diagnostic$1(invalidCode, `Could not parse ${path}: ${error.message}`))
};
let unsupportedFeature;
visit(document, {
Alias() {
unsupportedFeature ??= "aliases";
},
Node(_key, node) {
if (node.anchor) unsupportedFeature ??= "anchors";
else if (node.tag) unsupportedFeature ??= "explicit tags";
},
Pair(_key, pair) {
if (isScalar(pair.key) && pair.key.value === "<<") unsupportedFeature ??= "merge keys";
}
});
if (unsupportedFeature) return {
ok: false,
diagnostics: [diagnostic$1(unsupportedCode, `${path} uses ${unsupportedFeature}; ${label} must map directly to JSON data.`)]
};
try {
return {
ok: true,
value: document.toJSON()
};
} catch (error) {
return {
ok: false,
diagnostics: [diagnostic$1(invalidCode, `Could not parse ${path}: ${coerceErrorMessage(error)}`)]
};
}
}
//#endregion
//#region src/claws/openclaw-profile.ts
const MAX_PROFILE_BYTES = 262144;
const CLAW_PROFILE_PATH = "profiles/openclaw.yml";
const LEGACY_PROFILE_POINTER_KEY = "openclaw.config";
const LEGACY_PROFILE_POINTER_PATH = "$.metadata.openclaw.config";
const CONVENTIONAL_PROFILE_PATH = "$.profiles.openclaw";
function diagnostic(code, message, path = "$") {
return {
level: "error",
code,
phase: "parse",
path,
message
};
}
function warning(code, message, path) {
return {
level: "warning",
code,
phase: "parse",
path,
message
};
}
function isToolProfileId(value) {
return resolveClawToolProfileSnapshot({ profile: value }) !== void 0;
}
function migrateLegacyDynamicToolProfile(value) {
const profile = asOptionalRecord(value);
const agent = asOptionalRecord(profile?.agent);
const tools = asOptionalRecord(agent?.tools);
const toolProfile = tools?.profile;
if (!profile || !agent || !tools || typeof toolProfile !== "string" || !isToolProfileId(toolProfile) || tools.allow !== void 0) return { value };
if (toolProfile === "full") return { value };
const validationProbe = parseClawOpenClawProfile({
...profile,
agent: {
...agent,
tools: {
...tools,
profile: "minimal"
}
}
});
if (!validationProbe.ok) return { value };
const validatedTools = validationProbe.profile.agent.tools;
if (!validatedTools) return { value };
const selection = {
...validatedTools,
profile: toolProfile
};
const legacyProfile = {
...validationProbe.profile,
agent: {
...validationProbe.profile.agent,
tools: selection
}
};
const migrated = materializeClawToolProfile({ tools: selection }, { allowLegacyDynamicProfile: true });
return {
value: {
...profile,
agent: {
...agent,
tools: migrated.tools
}
},
legacyProfile
};
}
async function readProfileFile(packageRoot, path) {
return (await (await root(packageRoot)).read(path, {
hardlinks: "reject",
maxBytes: MAX_PROFILE_BYTES,
nonBlockingRead: true,
symlinks: "reject"
})).buffer;
}
/**
* Resolves the OpenClaw profile for a package.
*
* `profiles/openclaw.yml` is the conventional location. The retired
* `metadata.openclaw.config` pointer is still read for compatibility with
* packages published against the released contract; it reports a deprecation
* warning instead of failing, and only errors when it is malformed or conflicts
* with a conventional profile.
*/
async function readClawOpenClawProfile(params) {
const conventionalExists = await (await root(params.packageRoot)).exists(CLAW_PROFILE_PATH);
const legacyPointer = params.metadata?.[LEGACY_PROFILE_POINTER_KEY];
const diagnostics = [];
let declaredPath = CLAW_PROFILE_PATH;
let diagnosticPath = CONVENTIONAL_PROFILE_PATH;
if (legacyPointer !== void 0) {
if (legacyPointer.includes("\\") || !isSafeClawRelativePath(legacyPointer) || !/\.ya?ml$/i.test(legacyPointer)) return {
ok: false,
diagnostics: [diagnostic("invalid_openclaw_profile_path", `metadata.${LEGACY_PROFILE_POINTER_KEY} must reference a forward-slash package-relative .yml or .yaml file.`, LEGACY_PROFILE_POINTER_PATH)]
};
if (conventionalExists && legacyPointer !== CLAW_PROFILE_PATH) return {
ok: false,
diagnostics: [diagnostic("conflicting_openclaw_profile_pointer", `metadata.${LEGACY_PROFILE_POINTER_KEY} references ${legacyPointer} while ${CLAW_PROFILE_PATH} also exists; keep only ${CLAW_PROFILE_PATH}.`, LEGACY_PROFILE_POINTER_PATH)]
};
declaredPath = legacyPointer;
diagnosticPath = LEGACY_PROFILE_POINTER_PATH;
diagnostics.push(warning("deprecated_openclaw_profile_pointer", `metadata.${LEGACY_PROFILE_POINTER_KEY} is deprecated; move the profile to ${CLAW_PROFILE_PATH} and remove the metadata entry.`, LEGACY_PROFILE_POINTER_PATH));
} else if (!conventionalExists) return { ok: true };
let raw;
try {
raw = await readProfileFile(params.packageRoot, declaredPath);
} catch (error) {
const unsafe = error instanceof FsSafeError && (error.code === "hardlink" || error.code === "symlink" || error.code === "path-mismatch");
const tooLarge = error instanceof FsSafeError && error.code === "too-large";
return {
ok: false,
diagnostics: [diagnostic(unsafe ? "openclaw_profile_unsafe" : tooLarge ? "openclaw_profile_too_large" : "openclaw_profile_read_failed", unsafe ? "The OpenClaw profile must be a regular, non-symlinked, non-hardlinked file." : tooLarge ? `The OpenClaw profile exceeds ${MAX_PROFILE_BYTES} bytes.` : `Could not read ${declaredPath}: ${error.message}`, diagnosticPath)]
};
}
const text = raw.toString("utf8");
const yaml = parseClawYaml(text.startsWith("") ? text.slice(1) : text, declaredPath, "profile");
if (!yaml.ok) return yaml;
const migration = params.allowLegacyDynamicToolProfile ? migrateLegacyDynamicToolProfile(yaml.value) : { value: yaml.value };
const parsed = parseClawOpenClawProfile(migration.value);
if (!parsed.ok) return {
ok: false,
diagnostics: parsed.diagnostics.map((entry) => ({
...entry,
path: `${diagnosticPath}${entry.path.slice(1)}`
}))
};
return {
ok: true,
profile: parsed.profile,
...migration.legacyProfile ? { legacyProfile: migration.legacyProfile } : {},
raw,
path: declaredPath,
...diagnostics.length > 0 ? { diagnostics } : {}
};
}
//#endregion
//#region src/claws/source-limits.ts
const MAX_CLAW_MANIFEST_BYTES = 1048576;
const MAX_MANAGED_FILE_BYTES = 1048576;
const MAX_MANAGED_WORKSPACE_BYTES = 4 * MAX_MANAGED_FILE_BYTES;
//#endregion
//#region src/claws/reader.ts
const CLAW_MARKDOWN_FILENAME = "CLAW.md";
const MAX_CLAW_PACKAGE_JSON_BYTES = 262144;
async function readBoundedFile(path, maxBytes) {
return (await (await root(dirname(path))).read(basename(path), {
hardlinks: "reject",
maxBytes,
nonBlockingRead: true,
symlinks: "reject"
})).buffer;
}
function fileDiagnostic(code, message, path = "$") {
return {
level: "error",
code,
phase: "parse",
path,
message
};
}
function isContained(root, candidate) {
const child = relative(root, candidate);
return child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child);
}
function updateSnapshotHash(hash, label, bytes) {
hash.update(`${Buffer.byteLength(label, "utf8")}:${label}:${bytes.byteLength}:`, "utf8");
hash.update(bytes);
}
function workspaceSourceDiagnostic(error, sourcePath) {
if (error instanceof FsSafeError && error.code === "too-large") return fileDiagnostic("workspace_source_too_large", `Workspace source ${JSON.stringify(sourcePath)} exceeds ${MAX_MANAGED_FILE_BYTES} bytes.`, "$.workspace");
if (error instanceof FsSafeError && (error.code === "symlink" || error.code === "hardlink" || error.code === "path-mismatch") || error instanceof Error && error.message.includes("symlinked directory")) return fileDiagnostic("workspace_source_unsafe", `Workspace source ${JSON.stringify(sourcePath)} must be a regular, non-symlinked, non-hardlinked file.`, "$.workspace");
return fileDiagnostic("workspace_source_invalid", `Workspace source ${JSON.stringify(sourcePath)} must resolve inside the Claw source.`, "$.workspace");
}
async function buildDevelopmentSnapshot(params) {
const hash = createHash("sha256");
let byteLength = 0;
const add = (label, bytes) => {
updateSnapshotHash(hash, label, bytes);
byteLength += bytes.byteLength;
};
const snapshotFile = (bytes) => ({
byteLength: bytes.byteLength,
digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`
});
const manifest = snapshotFile(params.manifestRaw);
const openClawProfile = params.openClawProfile ? {
sourcePath: params.openClawProfile.path.replaceAll("\\", "/"),
...snapshotFile(params.openClawProfile.raw)
} : void 0;
add("canonical-source", Buffer.from(params.source.manifestPath, "utf8"));
add("manifest", params.manifestRaw);
if (params.openClawProfile) add(`profile:${params.openClawProfile.path.replaceAll("\\", "/")}`, params.openClawProfile.raw);
if (params.source.kind === "package") {
const packageJson = params.source.packageJsonRaw;
if (!packageJson) return {
ok: false,
diagnostics: [fileDiagnostic("package_read_failed", "Could not snapshot package.json.")]
};
add("package.json", packageJson);
}
const sourceRoot = await root(params.source.packageRoot);
let packageBootstrap;
if (await sourceRoot.exists("BOOTSTRAP.md")) try {
const read = await sourceRoot.read("BOOTSTRAP.md", {
hardlinks: "reject",
maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES,
nonBlockingRead: true,
symlinks: "reject"
});
if (new TextDecoder("utf-8", { fatal: true }).decode(read.buffer).trim().length === 0) return {
ok: false,
diagnostics: [fileDiagnostic("package_bootstrap_empty", "Package-root BOOTSTRAP.md must contain first-run instructions.", "$.bootstrap")]
};
const digest = `sha256:${createHash("sha256").update(read.buffer).digest("hex")}`;
add("bootstrap:BOOTSTRAP.md", read.buffer);
packageBootstrap = {
sourcePath: "BOOTSTRAP.md",
realPath: read.realPath,
byteLength: read.buffer.byteLength,
digest
};
} catch (error) {
const tooLarge = error instanceof FsSafeError && error.code === "too-large";
return {
ok: false,
diagnostics: [fileDiagnostic(tooLarge ? "package_bootstrap_too_large" : "package_bootstrap_invalid", tooLarge ? `Package-root BOOTSTRAP.md exceeds ${MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES} bytes.` : `Package-root BOOTSTRAP.md must be a safe UTF-8 regular file: ${error.message}`, "$.bootstrap")]
};
}
const declaredSources = [...Object.values(params.manifest.workspace.bootstrapFiles).filter((entry) => entry !== void 0).map((entry) => entry.source), ...params.manifest.workspace.files.map((entry) => entry.source)].toSorted((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
const openedSources = [];
const workspaceSources = [];
try {
let workspaceByteLength = 0;
for (const sourcePath of declaredSources) try {
await assertNoSymlinkParents({
rootDir: params.source.packageRoot,
targetPath: resolve(params.source.packageRoot, sourcePath),
allowMissing: false,
messagePrefix: "Workspace source"
});
const opened = await sourceRoot.open(sourcePath, {
hardlinks: "reject",
symlinks: "reject"
});
if (opened.stat.size > 1048576) {
await opened[Symbol.asyncDispose]();
throw new FsSafeError("too-large", `file exceeds limit of ${MAX_MANAGED_FILE_BYTES} bytes (got ${opened.stat.size})`);
}
workspaceByteLength += opened.stat.size;
openedSources.push({
sourcePath,
opened
});
} catch (error) {
return {
ok: false,
diagnostics: [workspaceSourceDiagnostic(error, sourcePath)]
};
}
if (workspaceByteLength > 4194304) return {
ok: false,
diagnostics: [fileDiagnostic("workspace_sources_too_large", `Workspace sources exceed ${MAX_MANAGED_WORKSPACE_BYTES} aggregate bytes.`, "$.workspace")]
};
let readWorkspaceByteLength = 0;
for (const { sourcePath, opened } of openedSources) {
const bytes = await opened.handle.readFile();
if (bytes.byteLength > 1048576) return {
ok: false,
diagnostics: [workspaceSourceDiagnostic(new FsSafeError("too-large", "workspace source grew while reading"), sourcePath)]
};
readWorkspaceByteLength += bytes.byteLength;
if (readWorkspaceByteLength > 4194304) return {
ok: false,
diagnostics: [fileDiagnostic("workspace_sources_too_large", `Workspace sources exceed ${MAX_MANAGED_WORKSPACE_BYTES} aggregate bytes.`, "$.workspace")]
};
const normalizedSourcePath = sourcePath.replaceAll("\\", "/");
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
add(`workspace:${sourcePath.replaceAll("\\", "/")}`, bytes);
workspaceSources.push({
sourcePath: normalizedSourcePath,
realPath: opened.realPath,
byteLength: bytes.byteLength,
digest
});
}
} finally {
await Promise.all(openedSources.map(({ opened }) => opened[Symbol.asyncDispose]()));
}
return {
ok: true,
integrity: `sha256:${hash.digest("hex")}`,
byteLength,
manifest,
...openClawProfile ? { openClawProfile } : {},
workspaceSources,
...packageBootstrap ? { packageBootstrap } : {}
};
}
function parsePackageJson(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return;
const record = value;
const openclaw = record.openclaw;
if (!openclaw || typeof openclaw !== "object" || Array.isArray(openclaw)) return;
const claw = openclaw.claw;
if (typeof record.name !== "string" || !isCanonicalClawHubPackageName(record.name) || typeof record.version !== "string" || !isExactSemVer(record.version) || typeof claw !== "string" || claw.trim() === "") return;
return {
name: record.name,
version: record.version,
openclaw: { claw }
};
}
async function readJson(path, code, maxBytes) {
let raw;
try {
raw = await readBoundedFile(path, maxBytes);
} catch (error) {
const tooLarge = error instanceof RangeError || error instanceof FsSafeError && error.code === "too-large";
return {
ok: false,
diagnostics: [fileDiagnostic(tooLarge ? `${code}_too_large` : code, tooLarge ? `${path} exceeds ${maxBytes} bytes.` : `Could not read ${path}: ${error.message}`)]
};
}
try {
return {
ok: true,
raw,
value: JSON.parse(raw.toString("utf8"))
};
} catch (error) {
return {
ok: false,
diagnostics: [fileDiagnostic("invalid_json", `Could not parse ${path}: ${error.message}`)]
};
}
}
function parseClawMarkdown(raw, path) {
const markdown = raw.length >= 3 && raw[0] === 239 && raw[1] === 187 && raw[2] === 191 ? raw.subarray(3) : raw;
const match = markdown.toString("latin1").match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!match) return {
ok: false,
diagnostics: [fileDiagnostic("missing_claw_frontmatter", `${path} must start with a YAML frontmatter block delimited by --- lines.`)]
};
const frontmatterBytes = Buffer.from(match[1] ?? "", "latin1");
const body = markdown.subarray(match[0].length);
let frontmatter;
try {
frontmatter = new TextDecoder("utf-8", { fatal: true }).decode(frontmatterBytes);
new TextDecoder("utf-8", { fatal: true }).decode(body);
} catch {
return {
ok: false,
diagnostics: [fileDiagnostic("invalid_claw_markdown_utf8", `${path} must contain valid UTF-8.`)]
};
}
const parsed = parseClawYaml(frontmatter, path, "frontmatter");
return parsed.ok ? {
...parsed,
body
} : parsed;
}
function parseClawManifestDocument(raw, path) {
if (basename(path).toLowerCase() === CLAW_MARKDOWN_FILENAME.toLowerCase()) return parseClawMarkdown(raw, path);
try {
return {
ok: true,
value: JSON.parse(raw.toString("utf8"))
};
} catch (error) {
return {
ok: false,
diagnostics: [fileDiagnostic("invalid_json", `Could not parse ${path}: ${error.message}`)]
};
}
}
async function readClawDocument(path, code, manifestFormatPath = path) {
let raw;
try {
raw = await readBoundedFile(path, MAX_CLAW_MANIFEST_BYTES);
} catch (error) {
const tooLarge = error instanceof RangeError || error instanceof FsSafeError && error.code === "too-large";
return {
ok: false,
diagnostics: [fileDiagnostic(tooLarge ? `${code}_too_large` : code, tooLarge ? `${path} exceeds ${MAX_CLAW_MANIFEST_BYTES} bytes.` : `Could not read ${path}: ${error.message}`)]
};
}
const parsed = parseClawManifestDocument(raw, manifestFormatPath);
return parsed.ok ? {
...parsed,
raw
} : parsed;
}
async function resolvePackageSource(packageRoot) {
const packageRootReal = await realpath(packageRoot).catch(() => void 0);
if (!packageRootReal) return {
ok: false,
diagnostics: [fileDiagnostic("package_read_failed", `Could not resolve ${packageRoot}.`)]
};
const packageJsonResult = await readJson(resolve(packageRootReal, "package.json"), "package_read_failed", MAX_CLAW_PACKAGE_JSON_BYTES);
if (!packageJsonResult.ok) return packageJsonResult;
const packageJson = parsePackageJson(packageJsonResult.value);
if (!packageJson) return {
ok: false,
diagnostics: [fileDiagnostic("invalid_package_metadata", "package.json must declare non-empty name, version, and openclaw.claw fields.")]
};
if (isAbsolute(packageJson.openclaw.claw)) return {
ok: false,
diagnostics: [fileDiagnostic("manifest_escapes_package", "openclaw.claw must be package-relative.")]
};
const declaredManifestPath = resolve(packageRootReal, packageJson.openclaw.claw);
const manifestPath = await realpath(declaredManifestPath).catch(() => void 0);
if (!manifestPath || !isContained(packageRootReal, manifestPath)) return {
ok: false,
diagnostics: [fileDiagnostic("manifest_escapes_package", "The declared Claw manifest must resolve inside its package root.")]
};
return {
ok: true,
source: {
kind: "package",
name: packageJson.name,
version: packageJson.version,
packageRoot: packageRootReal,
manifestPath,
packageJsonRaw: packageJsonResult.raw,
manifestFormatPath: declaredManifestPath
}
};
}
async function resolveSource(path) {
const inputPath = resolve(path);
const inputStat = await stat(inputPath).catch(() => void 0);
if (!inputStat) return {
ok: false,
diagnostics: [fileDiagnostic("read_failed", `Could not resolve Claw source ${inputPath}.`)]
};
if (inputStat.isDirectory()) return resolvePackageSource(inputPath);
if (!inputStat.isFile()) return {
ok: false,
diagnostics: [fileDiagnostic("unsupported_source", "Claw source must be a file or directory.")]
};
const manifestPath = await realpath(inputPath);
const packageRoot = await realpath(dirname(manifestPath));
return {
ok: true,
source: {
kind: "development",
name: `local:${basename(manifestPath).replace(/\.json$/i, "")}`,
version: "0.0.0-development",
packageRoot,
manifestPath,
manifestFormatPath: inputPath
}
};
}
async function readClawManifestFile(path, options = {}) {
const sourceResult = await resolveSource(path);
if (!sourceResult.ok) return sourceResult;
const manifestResult = await readClawDocument(sourceResult.source.manifestPath, "read_failed", sourceResult.source.manifestFormatPath);
if (!manifestResult.ok) return manifestResult;
const parsed = parseClawManifest(manifestResult.value);
if (!parsed.ok) return parsed;
const hasMarkdownBody = manifestResult.body !== void 0 && manifestResult.body.toString("utf8").trim().length > 0;
if (hasMarkdownBody && clawManifestWorkspaceConflictsWithPath(parsed.manifest, "SOUL.md")) return {
ok: false,
diagnostics: [fileDiagnostic("claw_body_soul_conflict", "CLAW.md body content and an explicit SOUL.md workspace declaration cannot both be present.", "$.workspace")]
};
const allowLegacyDynamicToolProfile = options.allowLegacyDynamicToolProfile === true || (options.authorizeLegacyDynamicToolProfile ? await options.authorizeLegacyDynamicToolProfile({
manifest: parsed.manifest,
source: {
kind: sourceResult.source.kind,
name: sourceResult.source.name,
version: sourceResult.source.version,
packageRoot: sourceResult.source.packageRoot,
manifestPath: sourceResult.source.manifestPath
}
}) : false);
const profile = await readClawOpenClawProfile({
packageRoot: sourceResult.source.packageRoot,
metadata: parsed.manifest.metadata,
...allowLegacyDynamicToolProfile ? { allowLegacyDynamicToolProfile: true } : {}
});
if (!profile.ok) return profile;
const snapshot = await buildDevelopmentSnapshot({
source: sourceResult.source,
manifest: parsed.manifest,
manifestRaw: manifestResult.raw,
...profile.raw && profile.path ? { openClawProfile: {
path: profile.path,
raw: profile.raw
} } : {}
});
if (!snapshot.ok) return snapshot;
const resolvedSource = sourceResult.source;
const source = {
kind: resolvedSource.kind,
name: resolvedSource.name,
version: resolvedSource.version,
packageRoot: resolvedSource.packageRoot,
manifestPath: resolvedSource.manifestPath,
integrityKind: "development-snapshot",
integrity: snapshot.integrity,
byteLength: snapshot.byteLength
};
return {
ok: true,
manifest: parsed.manifest,
...hasMarkdownBody ? { clawMarkdownBody: manifestResult.body } : {},
...snapshot.packageBootstrap ? { packageBootstrap: snapshot.packageBootstrap } : {},
...profile.profile ? { openClawProfile: profile.profile } : {},
...profile.legacyProfile ? { legacyOpenClawProfile: profile.legacyProfile } : {},
source,
snapshot: {
manifest: snapshot.manifest,
...snapshot.openClawProfile ? { openClawProfile: snapshot.openClawProfile } : {},
workspaceSources: snapshot.workspaceSources,
...snapshot.packageBootstrap ? { packageBootstrap: snapshot.packageBootstrap } : {}
},
diagnostics: [...parsed.diagnostics, ...profile.diagnostics ?? []]
};
}
//#endregion
export { digestClawMcpServer as C, readClawMcpServerRefsByName as D, readClawMcpServerRefs as E, reconcileClawMcpServerRefs as O, deleteClawMcpServerRef as S, planClawMcpServerRemoval as T, CLAW_INSPECT_RESULT_SCHEMA_VERSION as _, MAX_MANAGED_WORKSPACE_BYTES as a, ClawMcpInstallError as b, parseClawOpenClawProfile as c, isCanonicalClawHubPackageName as d, isPortableClawAvatar as f, CLAW_BOOTSTRAP_FILE_NAMES as g, CLAW_ADD_PLAN_SCHEMA_VERSION as h, MAX_MANAGED_FILE_BYTES as i, upsertClawMcpServerRef as k, materializeClawToolProfile as l, portableClawPathKey as m, readClawManifestFile as n, clawManifestWorkspaceConflictsWithPath as o, isSafeClawRelativePath as p, MAX_CLAW_MANIFEST_BYTES as r, parseClawManifest as s, parseClawMarkdown as t, resolveClawToolProfileSnapshot as u, CLAW_OUTPUT_STABILITY as v, installClawMcpServers as w, clawMcpRemovalSelector as x, CLAW_MCP_REF_SCHEMA_VERSION as y };