Files
clawhub/packages/schema/dist/claws.js
T
Gio Della-Libera 348851eeb9 feat(claws): align package layers with schema v1 (#3328)
Adds conventional harness profiles, package-root BOOTSTRAP.md, strict OpenClaw validation, portable path hardening, and an official upstream contract pin.
2026-08-09 07:46:06 -07:00

642 lines
27 KiB
JavaScript

import { ArkErrors, type } from "arktype";
import { Cron } from "croner";
import { caseFold } from "unicode-case-folding";
export const CLAW_SCHEMA_VERSION = 1;
export const CLAW_SUMMARY_AGENT_NAME_MAX_CHARS = 128;
export const CLAW_SUMMARY_AGENT_DESCRIPTION_MAX_CHARS = 1_024;
export const CLAW_BOOTSTRAP_FILE_NAMES = [
"AGENTS.md",
"SOUL.md",
"IDENTITY.md",
"TOOLS.md",
"HEARTBEAT.md",
];
const StringArraySchema = type("string[]");
const WorkspaceSourceSchema = type({ "+": "reject", source: "string" });
const ToolFilterSchema = type({
"+": "reject",
include: StringArraySchema.optional(),
exclude: StringArraySchema.optional(),
});
const StdioMcpServerSchema = type({
"+": "reject",
command: "string",
transport: '"stdio"?',
args: StringArraySchema.optional(),
env: type({ "[string]": "string" }).optional(),
toolFilter: ToolFilterSchema.optional(),
timeout: "number?",
connectTimeout: "number?",
});
const RemoteMcpServerSchema = type({
"+": "reject",
url: "string",
transport: '"sse"|"streamable-http"',
auth: '"oauth"?',
toolFilter: ToolFilterSchema.optional(),
timeout: "number?",
connectTimeout: "number?",
});
export const ClawManifestSchema = type({
"+": "reject",
schemaVersion: "1",
agent: {
"+": "reject",
id: "string",
name: "string?",
description: "string?",
identity: type({
"+": "reject",
name: "string?",
theme: "string?",
emoji: "string?",
avatar: "string?",
}).optional(),
},
metadata: type({ "[string]": "string" }).optional(),
workspace: type({
"+": "reject",
bootstrapFiles: type({
"+": "reject",
"AGENTS.md": WorkspaceSourceSchema.optional(),
"SOUL.md": WorkspaceSourceSchema.optional(),
"IDENTITY.md": WorkspaceSourceSchema.optional(),
"TOOLS.md": WorkspaceSourceSchema.optional(),
"HEARTBEAT.md": WorkspaceSourceSchema.optional(),
}).optional(),
files: type({ "+": "reject", source: "string", path: "string" }).array().optional(),
}).optional(),
packages: type({
"+": "reject",
kind: '"skill"|"plugin"',
source: '"clawhub"',
ref: "string",
version: "string",
})
.array()
.optional(),
mcpServers: type({
"[string]": StdioMcpServerSchema.or(RemoteMcpServerSchema),
}).optional(),
cronJobs: type({
"+": "reject",
id: "string",
name: "string?",
schedule: { "+": "reject", cron: "string", timezone: "string" },
session: '"main"|"isolated"',
message: "string",
delivery: type({
"+": "reject",
mode: '"none"|"announce"',
channel: '"last"?',
}).optional(),
})
.array()
.optional(),
});
/** Builds the v1 summary structure for both the public schema and durable storage validators. */
export function createClawManifestSummarySchema(adapter) {
return adapter.object({
schemaVersion: adapter.literalOne,
agent: adapter.object({
id: adapter.string,
name: adapter.optional(adapter.boundedString(CLAW_SUMMARY_AGENT_NAME_MAX_CHARS)),
description: adapter.optional(adapter.boundedString(CLAW_SUMMARY_AGENT_DESCRIPTION_MAX_CHARS)),
}),
workspace: adapter.object({
bootstrapFiles: adapter.stringArray,
fileCount: adapter.number,
}),
packages: adapter.object({
skillCount: adapter.number,
pluginCount: adapter.number,
}),
mcpServerCount: adapter.number,
cronJobCount: adapter.number,
});
}
export const ClawManifestSummarySchema = createClawManifestSummarySchema({
literalOne: type("1"),
string: type("string"),
number: type("number"),
stringArray: type("string[]"),
boundedString: (maxCharacters) => type("string").narrow((value) => Array.from(value).length <= maxCharacters),
optional: (schema) => schema.optional(),
object: (fields) => type({ "+": "reject", ...fields }),
});
export const CLAW_MANIFEST_VALIDATION_PHASE = "schema";
export const CLAW_MANIFEST_VALIDATION_CODES = {
invalidManifestShape: "claw_v1_invalid_manifest_shape",
invalidAgentId: "claw_v1_invalid_agent_id",
nonCanonicalString: "claw_v1_non_canonical_string",
emptyList: "claw_v1_empty_list",
legacyProfilePointer: "claw_v1_legacy_profile_pointer",
reservedWorkspaceTarget: "claw_v1_reserved_workspace_target",
unsafePath: "claw_v1_unsafe_path",
duplicateWorkspaceDestination: "claw_v1_duplicate_workspace_destination",
invalidAvatar: "claw_v1_invalid_avatar",
undeclaredAvatar: "claw_v1_undeclared_avatar",
invalidPackageReference: "claw_v1_invalid_package_reference",
invalidPackageVersion: "claw_v1_invalid_package_version",
duplicatePackage: "claw_v1_duplicate_package",
invalidMcpServerId: "claw_v1_invalid_mcp_server_id",
invalidMcpUrl: "claw_v1_invalid_mcp_url",
mcpUrlCredentials: "claw_v1_mcp_url_credentials",
unpinnedMcpPackage: "claw_v1_unpinned_mcp_package",
invalidToolFilter: "claw_v1_invalid_tool_filter",
duplicateToolFilter: "claw_v1_duplicate_tool_filter",
invalidEnvironmentKey: "claw_v1_invalid_environment_key",
blockedEnvironmentKey: "claw_v1_blocked_environment_key",
invalidEnvironmentReference: "claw_v1_invalid_environment_reference",
invalidTimeout: "claw_v1_invalid_timeout",
invalidCronJobId: "claw_v1_invalid_cron_job_id",
invalidCronSchedule: "claw_v1_invalid_cron_schedule",
invalidCronDelivery: "claw_v1_invalid_cron_delivery",
duplicateCronJob: "claw_v1_duplicate_cron_job",
};
const AGENT_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
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 ENV_REFERENCE_PATTERN = /^\$\{[A-Z_][A-Z0-9_]*\}$/;
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 UNICODE_CONTROL_CHARACTER = /\p{Cc}/u;
const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
const AVATAR_MAX_DATA_URL_CHARS = 2_796_230;
const AVATAR_DATA_URL_PATTERN = /^data:image\/[^,]*,/i;
const AVATAR_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp|svg)$/i;
const BASE64_PAYLOAD_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
const PORTABLE_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
// Versioned compatibility artifact for the OpenClaw policy applied by the v1 consumer.
export const OPENCLAW_CLAW_HOST_ENV_POLICY_V1 = {
contractVersion: 1,
source: {
repository: "openclaw/openclaw",
commit: "f50a918af102b950721c1b380f7eb8af4e0389d1",
path: "src/infra/host-env-security-policy.json",
sha256: "4639dcfa157d4fa4c2fb84f8c5347ffe2252eb4ddb3d818afd7f87d68173b92b",
},
blockedKeys: [
"NODE_OPTIONS",
"NODE_PATH",
"NODE_REDIRECT_WARNINGS",
"NODE_REPL_EXTERNAL_MODULE",
"NODE_REPL_HISTORY",
"NODE_V8_COVERAGE",
"PYTHONHOME",
"PYTHONPATH",
"PERL5LIB",
"PERL5OPT",
"RUBYLIB",
"RUBYOPT",
"BASHOPTS",
"BASH_ENV",
"ENV",
"KSH_ENV",
"BROWSER",
"GIT_ALLOW_PROTOCOL",
"GIT_EDITOR",
"GIT_EXTERNAL_DIFF",
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_COMMON_DIR",
"GIT_EXEC_PATH",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_NAMESPACE",
"GIT_PROTOCOL_FROM_USER",
"GIT_SEQUENCE_EDITOR",
"GIT_TEMPLATE_DIR",
"GIT_SSL_NO_VERIFY",
"GIT_SSL_CAINFO",
"GIT_SSL_CAPATH",
"CC",
"CPP",
"CXX",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
"CARGO_BUILD_RUSTDOC",
"RUSTC",
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"RUSTDOC",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"SHELL",
"SHELLOPTS",
"PS4",
"GCONV_PATH",
"IFS",
"SSLKEYLOGFILE",
"JAVA_OPTS",
"JAVA_TOOL_OPTIONS",
"_JAVA_OPTIONS",
"JDK_JAVA_OPTIONS",
"PYTHONBREAKPOINT",
"DOTNET_STARTUP_HOOKS",
"DOTNET_ADDITIONAL_DEPS",
"FPATH",
"GLIBC_TUNABLES",
"MAVEN_OPTS",
"MAKE",
"MAKEFLAGS",
"MFLAGS",
"SBT_OPTS",
"GRADLE_OPTS",
"ANT_OPTS",
"HGRCPATH",
"HGEDITOR",
"HGMERGE",
"EXINIT",
"VIMINIT",
"MYVIMRC",
"GVIMINIT",
"LUA_INIT",
"LUA_INIT_5_1",
"LUA_INIT_5_2",
"LUA_INIT_5_3",
"LUA_INIT_5_4",
"EMACSLOADPATH",
"RUBYSHELL",
"GIT_HOOK_PATH",
"SVN_EDITOR",
"SVN_SSH",
"BZR_EDITOR",
"BZR_SSH",
"BZR_PLUGIN_PATH",
"SUDO_ASKPASS",
"JULIA_EDITOR",
"CONFIG_SITE",
"CONFIG_SHELL",
"CMAKE_TOOLCHAIN_FILE",
"CATALINA_OPTS",
"CORECLR_PROFILER",
"HELM_PLUGINS",
"PACKER_PLUGIN_PATH",
"VAGRANT_VAGRANTFILE",
"ERL_AFLAGS",
"ERL_FLAGS",
"ERL_ZFLAGS",
"ELIXIR_ERL_OPTIONS",
"R_ENVIRON",
"R_PROFILE",
"R_ENVIRON_USER",
"R_PROFILE_USER",
"TCLLIBPATH",
"HOSTALIASES",
],
blockedPrefixes: ["DYLD_", "LD_", "BASH_FUNC_"],
};
const BLOCKED_PROCESS_ENV_KEYS = new Set(OPENCLAW_CLAW_HOST_ENV_POLICY_V1.blockedKeys);
const BLOCKED_PROCESS_ENV_PREFIXES = OPENCLAW_CLAW_HOST_ENV_POLICY_V1.blockedPrefixes;
function isStrictNonEmpty(value) {
return value.length > 0 && value === value.trim();
}
function isBlockedProcessEnvKey(value) {
const upper = value.toUpperCase();
return (BLOCKED_PROCESS_ENV_KEYS.has(upper) ||
BLOCKED_PROCESS_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix)));
}
function isSafePackagePath(value) {
const normalized = value.replaceAll("\\", "/");
if (!isStrictNonEmpty(normalized) ||
normalized.startsWith("/") ||
/^[A-Za-z]:\//.test(normalized)) {
return false;
}
return normalized
.split("/")
.every((segment) => segment !== "" &&
segment !== "." &&
segment !== ".." &&
!WINDOWS_INVALID_PATH_CHARS.test(segment) &&
!UNICODE_CONTROL_CHARACTER.test(segment) &&
!segment.endsWith(".") &&
!segment.endsWith(" ") &&
!WINDOWS_RESERVED_PATH_SEGMENT.test(segment));
}
function portablePathKey(value) {
return caseFold(value.replaceAll("\\", "/").normalize("NFC"));
}
function isPortableAvatar(value) {
if (AVATAR_DATA_URL_PATTERN.test(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 byteLength = base64
? (payload.length / 4) * 3 - (payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0)
: new TextEncoder().encode(decodeURIComponent(payload)).byteLength;
return byteLength > 0 && byteLength <= AVATAR_MAX_BYTES;
}
catch {
return false;
}
}
return isSafePackagePath(value) && AVATAR_EXTENSION_PATTERN.test(value);
}
function conflictsWithWorkspaceTarget(targets, candidate) {
for (const target of targets) {
if (target === candidate ||
target.startsWith(`${candidate}/`) ||
candidate.startsWith(`${target}/`)) {
return true;
}
}
return false;
}
function packageManagerArtifacts(command, args) {
const executable = command
.split(/[\\/]/)
.at(-1)
?.replace(/\.(?:cmd|exe)$/i, "")
.toLowerCase();
let start = 0;
let scansOptionsAfterPositional = false;
if (executable === "npm") {
if (args[0] !== "exec" && args[0] !== "x")
return undefined;
start = 1;
scansOptionsAfterPositional = true;
}
else if (executable === "bun") {
if (args[0] !== "x")
return undefined;
start = 1;
}
else if (executable === "pnpm" || executable === "yarn") {
if (args[0] !== "dlx")
return undefined;
start = 1;
}
else if (executable !== "npx" && executable !== "pnpx" && executable !== "bunx") {
return undefined;
}
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("--package=".length));
continue;
}
if (value === "-y" || value === "--yes")
continue;
if (value.startsWith("-"))
return [""];
if (positional === undefined && !value.startsWith("-")) {
positional = value;
if (!scansOptionsAfterPositional)
break;
}
}
return selected.length > 0 ? selected : [positional ?? ""];
}
function truncateSummaryText(value, maxChars) {
if (!value)
return undefined;
return Array.from(value).slice(0, maxChars).join("");
}
function isPackageManagerArtifactPinned(command, args) {
const artifacts = packageManagerArtifacts(command, args);
if (artifacts === undefined)
return undefined;
return artifacts.every((artifact) => {
const separator = artifact.lastIndexOf("@");
const scopedSlash = artifact.startsWith("@") ? artifact.indexOf("/") : -1;
return (separator > 0 &&
separator > scopedSlash &&
EXACT_VERSION_PATTERN.test(artifact.slice(separator + 1)));
});
}
function validationIssue(code, path, message) {
return { code, phase: CLAW_MANIFEST_VALIDATION_PHASE, path, message };
}
function arkIssues(errors) {
return Array.from(errors, (error) => validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidManifestShape, error.path.length > 0 ? `$.${error.path.join(".")}` : "$", error.description ?? "Manifest must match the strict Claw v1 shape."));
}
function pushNonEmpty(issues, path, value) {
if (value !== undefined && !isStrictNonEmpty(value)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.nonCanonicalString, path, "Must be non-empty without leading or trailing whitespace."));
}
}
function pushNonEmptyArray(issues, path, values, requireEntry) {
if (requireEntry && values !== undefined && values.length === 0) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.emptyList, path, "Must contain at least one value."));
}
for (const [index, value] of (values ?? []).entries()) {
pushNonEmpty(issues, `${path}.${index}`, value);
}
}
export function validateClawManifest(value) {
const parsed = ClawManifestSchema(value);
if (parsed instanceof ArkErrors)
return { ok: false, issues: arkIssues(parsed) };
const issues = [];
if (!AGENT_ID_PATTERN.test(parsed.agent.id)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidAgentId, "$.agent.id", "Invalid portable agent id."));
}
pushNonEmpty(issues, "$.agent.name", parsed.agent.name);
pushNonEmpty(issues, "$.agent.description", parsed.agent.description);
for (const field of ["name", "theme", "emoji", "avatar"]) {
pushNonEmpty(issues, `$.agent.identity.${field}`, parsed.agent.identity?.[field]);
}
for (const key of Object.keys(parsed.metadata ?? {})) {
pushNonEmpty(issues, `$.metadata.${key}`, key);
}
if (Object.hasOwn(parsed.metadata ?? {}, "openclaw.config")) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.legacyProfilePointer, "$.metadata.openclaw.config", "metadata.openclaw.config is no longer supported; move the profile to profiles/openclaw.yml and remove the metadata entry."));
}
const workspaceTargets = new Set();
for (const name of CLAW_BOOTSTRAP_FILE_NAMES) {
const source = parsed.workspace?.bootstrapFiles?.[name]?.source;
if (source === undefined)
continue;
if (!isSafePackagePath(source)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.unsafePath, `$.workspace.bootstrapFiles.${name}.source`, "Must be a safe package-relative path."));
}
workspaceTargets.add(portablePathKey(name));
}
for (const [index, file] of (parsed.workspace?.files ?? []).entries()) {
if (!isSafePackagePath(file.source)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.unsafePath, `$.workspace.files.${index}.source`, "Must be a safe package-relative path."));
}
if (!isSafePackagePath(file.path)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.unsafePath, `$.workspace.files.${index}.path`, "Must be a safe package-relative path."));
}
const destinationKey = portablePathKey(file.path);
if (destinationKey === portablePathKey("BOOTSTRAP.md")) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.reservedWorkspaceTarget, `$.workspace.files.${index}.path`, "Root BOOTSTRAP.md is reserved for the package-root seed-once bootstrap file."));
}
if (conflictsWithWorkspaceTarget(workspaceTargets, destinationKey)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.duplicateWorkspaceDestination, `$.workspace.files.${index}.path`, "Workspace destination is declared more than once."));
}
workspaceTargets.add(destinationKey);
}
const avatar = parsed.agent.identity?.avatar;
if (avatar !== undefined) {
if (!isPortableAvatar(avatar)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidAvatar, "$.agent.identity.avatar", "Must be a bounded image data URL or managed workspace-relative image path."));
}
else if (!AVATAR_DATA_URL_PATTERN.test(avatar) &&
!workspaceTargets.has(portablePathKey(avatar))) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.undeclaredAvatar, "$.agent.identity.avatar", "Workspace-relative avatar must match a workspace.files destination."));
}
}
const packageKeys = new Set();
for (const [index, pkg] of (parsed.packages ?? []).entries()) {
pushNonEmpty(issues, `$.packages.${index}.ref`, pkg.ref);
if (!PACKAGE_NAME_PATTERN.test(pkg.ref)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidPackageReference, `$.packages.${index}.ref`, "Must be a lowercase ClawHub package name."));
}
if (!EXACT_VERSION_PATTERN.test(pkg.version)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidPackageVersion, `$.packages.${index}.version`, "Must be an exact semantic version."));
}
const key = `${pkg.kind}:${pkg.source}:${pkg.ref.toLowerCase()}`;
if (packageKeys.has(key)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.duplicatePackage, `$.packages.${index}`, "Package is declared more than once."));
}
packageKeys.add(key);
}
for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
if (!AGENT_ID_PATTERN.test(name)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidMcpServerId, `$.mcpServers.${name}`, "Invalid MCP server name."));
}
if ("url" in server) {
pushNonEmpty(issues, `$.mcpServers.${name}.url`, server.url);
try {
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)) {
throw new Error("protocol");
}
if (url.username || url.password || url.hash) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.mcpUrlCredentials, `$.mcpServers.${name}.url`, "Must not contain embedded credentials or fragments."));
}
}
catch {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidMcpUrl, `$.mcpServers.${name}.url`, "Must use HTTPS, except HTTP on an exact loopback host."));
}
}
if ("command" in server) {
pushNonEmpty(issues, `$.mcpServers.${name}.command`, server.command);
pushNonEmptyArray(issues, `$.mcpServers.${name}.args`, server.args, false);
if (isPackageManagerArtifactPinned(server.command, server.args ?? []) === false) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.unpinnedMcpPackage, `$.mcpServers.${name}.args`, "Package-manager MCP commands must select one exact immutable package version."));
}
}
pushNonEmptyArray(issues, `$.mcpServers.${name}.toolFilter.include`, server.toolFilter?.include, true);
pushNonEmptyArray(issues, `$.mcpServers.${name}.toolFilter.exclude`, server.toolFilter?.exclude, true);
for (const field of ["include", "exclude"]) {
const seen = new Set();
for (const [index, entry] of (server.toolFilter?.[field] ?? []).entries()) {
if (entry.includes("?") || entry.includes("[") || entry.includes("]")) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidToolFilter, `$.mcpServers.${name}.toolFilter.${field}.${index}`, "Tool filters support only exact names and * wildcards."));
}
if (seen.has(entry)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.duplicateToolFilter, `$.mcpServers.${name}.toolFilter.${field}.${index}`, "Tool filter entries must be unique."));
}
seen.add(entry);
}
}
if ("env" in server) {
for (const [key, envValue] of Object.entries(server.env ?? {})) {
pushNonEmpty(issues, `$.mcpServers.${name}.env`, key);
if (!PORTABLE_ENV_KEY_PATTERN.test(key)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidEnvironmentKey, `$.mcpServers.${name}.env.${key}`, "Invalid portable environment key."));
}
if (isBlockedProcessEnvKey(key)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.blockedEnvironmentKey, `$.mcpServers.${name}.env.${key}`, "Environment key is blocked by the spawned-process safety policy."));
}
if (!ENV_REFERENCE_PATTERN.test(envValue)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidEnvironmentReference, `$.mcpServers.${name}.env.${key}`, "Must be an unresolved ${ENV_VAR} reference."));
}
}
}
if (server.timeout !== undefined && (!Number.isFinite(server.timeout) || server.timeout <= 0)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidTimeout, `$.mcpServers.${name}.timeout`, "Must be positive."));
}
if (server.connectTimeout !== undefined &&
(!Number.isFinite(server.connectTimeout) || server.connectTimeout <= 0)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidTimeout, `$.mcpServers.${name}.connectTimeout`, "Must be positive."));
}
}
const cronIds = new Set();
for (const [index, job] of (parsed.cronJobs ?? []).entries()) {
if (!AGENT_ID_PATTERN.test(job.id)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidCronJobId, `$.cronJobs.${index}.id`, "Invalid cron job id."));
}
pushNonEmpty(issues, `$.cronJobs.${index}.name`, job.name);
pushNonEmpty(issues, `$.cronJobs.${index}.schedule.cron`, job.schedule.cron);
pushNonEmpty(issues, `$.cronJobs.${index}.schedule.timezone`, job.schedule.timezone);
pushNonEmpty(issues, `$.cronJobs.${index}.message`, job.message);
try {
if (job.schedule.cron.trim().split(/\s+/).length !== 5)
throw new Error("fields");
const cron = new Cron(job.schedule.cron.trim(), {
timezone: job.schedule.timezone.trim(),
catch: false,
paused: true,
});
if (!cron.nextRun(new Date()))
throw new Error("No future run");
}
catch {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidCronSchedule, `$.cronJobs.${index}.schedule`, "Must contain a valid cron expression and timezone."));
}
if ((job.delivery?.mode === "none" && job.delivery.channel !== undefined) ||
(job.delivery?.mode === "announce" && job.delivery.channel !== "last")) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.invalidCronDelivery, `$.cronJobs.${index}.delivery`, 'Must be { mode: "none" } or { mode: "announce", channel: "last" }.'));
}
if (cronIds.has(job.id)) {
issues.push(validationIssue(CLAW_MANIFEST_VALIDATION_CODES.duplicateCronJob, `$.cronJobs.${index}.id`, "Cron job id is declared more than once."));
}
cronIds.add(job.id);
}
return issues.length > 0 ? { ok: false, issues } : { ok: true, manifest: parsed };
}
export function summarizeClawManifest(manifest, options = {}) {
const packages = manifest.packages ?? [];
const agentName = truncateSummaryText(manifest.agent.name, CLAW_SUMMARY_AGENT_NAME_MAX_CHARS);
const agentDescription = truncateSummaryText(manifest.agent.description, CLAW_SUMMARY_AGENT_DESCRIPTION_MAX_CHARS);
return {
schemaVersion: CLAW_SCHEMA_VERSION,
agent: {
id: manifest.agent.id,
...(agentName ? { name: agentName } : {}),
...(agentDescription ? { description: agentDescription } : {}),
},
workspace: {
bootstrapFiles: CLAW_BOOTSTRAP_FILE_NAMES.filter((name) => manifest.workspace?.bootstrapFiles?.[name] !== undefined ||
(name === "SOUL.md" && options.clawMarkdownBody === true)),
fileCount: manifest.workspace?.files?.length ?? 0,
},
packages: {
skillCount: packages.filter((pkg) => pkg.kind === "skill").length,
pluginCount: packages.filter((pkg) => pkg.kind === "plugin").length,
},
mcpServerCount: Object.keys(manifest.mcpServers ?? {}).length,
cronJobCount: manifest.cronJobs?.length ?? 0,
};
}
//# sourceMappingURL=claws.js.map