mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
fix(skills-catalog): resolve platform metadata fallbacks (#229)
* fix(skills-catalog): resolve platform metadata fallbacks * fix(skills-catalog): harden platform metadata guards
This commit is contained in:
@@ -183,16 +183,27 @@ jobs:
|
||||
done
|
||||
|
||||
# Build skill entry for index
|
||||
SKILL_DATA=$(jq -c --arg tag "$TAG" '{
|
||||
id: .name,
|
||||
name: .name,
|
||||
version: .version,
|
||||
description: .description,
|
||||
emoji: .openclaw.emoji,
|
||||
category: .openclaw.category,
|
||||
trust: .trust.level,
|
||||
tag: $tag
|
||||
}' "$MIRROR_DIR/skill.json")
|
||||
SKILL_DATA=$(jq -c --arg tag "$TAG" '
|
||||
def object_or_empty($value):
|
||||
if ($value | type) == "object" then $value else {} end;
|
||||
def object_field($name):
|
||||
object_or_empty(.[$name]?);
|
||||
def platform_meta:
|
||||
(.platform as $platform
|
||||
| if ($platform | type) == "string" then object_or_empty(.[$platform]?)
|
||||
else {}
|
||||
end);
|
||||
{
|
||||
id: .name,
|
||||
name: .name,
|
||||
version: .version,
|
||||
description: .description,
|
||||
emoji: (platform_meta.emoji // object_field("openclaw").emoji // object_field("hermes").emoji // object_field("nanoclaw").emoji // object_field("picoclaw").emoji // "📦"),
|
||||
category: (platform_meta.category // object_field("openclaw").category // object_field("hermes").category // object_field("nanoclaw").category // object_field("picoclaw").category // "utility"),
|
||||
trust: .trust.level,
|
||||
tag: $tag
|
||||
}
|
||||
' "$MIRROR_DIR/skill.json")
|
||||
|
||||
# Append to index (handle first entry without comma)
|
||||
if [ -f "public/skills/.first_done" ]; then
|
||||
|
||||
+42
-6
@@ -4,15 +4,41 @@ import { ArrowLeft, Copy, Check, Download, ExternalLink, FileText, Shield } from
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Footer } from '../components/Footer';
|
||||
import type { SkillJson, SkillChecksums } from '../types';
|
||||
import type { SkillJson, SkillChecksums, SkillPlatformMetadata } from '../types';
|
||||
import { defaultMarkdownComponents } from '../utils/markdownComponents';
|
||||
import { stripFrontmatter } from '../utils/markdownHelpers.mjs';
|
||||
|
||||
const PLATFORM_METADATA_KEYS = ['openclaw', 'hermes', 'nanoclaw', 'picoclaw'] as const;
|
||||
|
||||
const isProbablyHtmlDocument = (text: string): boolean => {
|
||||
const start = text.trimStart().slice(0, 200).toLowerCase();
|
||||
return start.startsWith('<!doctype html') || start.startsWith('<html');
|
||||
};
|
||||
|
||||
const isPlatformMetadataObject = (value: unknown): value is SkillPlatformMetadata => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const maybe = value as Record<string, unknown>;
|
||||
return 'emoji' in maybe || 'category' in maybe || 'triggers' in maybe;
|
||||
};
|
||||
|
||||
const resolvePlatformMetadata = (skill: SkillJson): SkillPlatformMetadata => {
|
||||
const platform = skill.platform;
|
||||
if (
|
||||
typeof platform === 'string' &&
|
||||
(PLATFORM_METADATA_KEYS as readonly string[]).includes(platform)
|
||||
) {
|
||||
const platformBlock = skill[platform as (typeof PLATFORM_METADATA_KEYS)[number]];
|
||||
if (isPlatformMetadataObject(platformBlock)) return platformBlock;
|
||||
}
|
||||
|
||||
for (const key of PLATFORM_METADATA_KEYS) {
|
||||
const fallbackBlock = skill[key];
|
||||
if (isPlatformMetadataObject(fallbackBlock)) return fallbackBlock;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const SkillDetail: React.FC = () => {
|
||||
const { skillId } = useParams<{ skillId: string }>();
|
||||
const [skillData, setSkillData] = useState<SkillJson | null>(null);
|
||||
@@ -144,6 +170,16 @@ export const SkillDetail: React.FC = () => {
|
||||
return skillData.homepage;
|
||||
}, [skillData]);
|
||||
|
||||
const platformMetadata = useMemo(
|
||||
() => (skillData ? resolvePlatformMetadata(skillData) : null),
|
||||
[skillData]
|
||||
);
|
||||
|
||||
const triggers = useMemo(() => {
|
||||
if (!platformMetadata || !Array.isArray(platformMetadata.triggers)) return [];
|
||||
return platformMetadata.triggers;
|
||||
}, [platformMetadata]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="py-16 text-center">
|
||||
@@ -180,14 +216,14 @@ export const SkillDetail: React.FC = () => {
|
||||
{/* Header */}
|
||||
<section className="flex flex-col md:flex-row md:items-start md:justify-between gap-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-4xl">{skillData.openclaw?.emoji || '📦'}</span>
|
||||
<span className="text-4xl">{platformMetadata?.emoji || '📦'}</span>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-1">{skillData.name}</h1>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="text-gray-500 font-mono">v{skillData.version}</span>
|
||||
{/* Category badge - hidden for now, uncomment when we have multiple categories
|
||||
<span className="text-gray-500 bg-clawd-800 px-2 py-0.5 rounded">
|
||||
{skillData.openclaw?.category || 'utility'}
|
||||
{platformMetadata?.category || 'utility'}
|
||||
</span>
|
||||
*/}
|
||||
</div>
|
||||
@@ -339,16 +375,16 @@ export const SkillDetail: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Category</dt>
|
||||
<dd className="text-white">{skillData.openclaw?.category}</dd>
|
||||
<dd className="text-white">{platformMetadata?.category || 'utility'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{skillData.openclaw?.triggers && skillData.openclaw.triggers.length > 0 && (
|
||||
{triggers.length > 0 && (
|
||||
<div className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6 space-y-4">
|
||||
<h3 className="font-bold text-white">Trigger Phrases</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{skillData.openclaw.triggers.slice(0, 8).map((trigger) => (
|
||||
{triggers.slice(0, 8).map((trigger) => (
|
||||
<span
|
||||
key={trigger}
|
||||
className="text-xs bg-clawd-700 text-gray-300 px-2 py-1 rounded"
|
||||
|
||||
@@ -168,15 +168,26 @@ EOF
|
||||
echo " ✓ Generated: checksums.json"
|
||||
|
||||
# Build skill entry for index
|
||||
SKILL_DATA=$(jq -c --arg tag "$TAG" '{
|
||||
id: .name,
|
||||
name: .name,
|
||||
version: .version,
|
||||
description: .description,
|
||||
emoji: .openclaw.emoji,
|
||||
category: .openclaw.category,
|
||||
tag: $tag
|
||||
}' "$SKILL_JSON")
|
||||
SKILL_DATA=$(jq -c --arg tag "$TAG" '
|
||||
def object_or_empty($value):
|
||||
if ($value | type) == "object" then $value else {} end;
|
||||
def object_field($name):
|
||||
object_or_empty(.[$name]?);
|
||||
def platform_meta:
|
||||
(.platform as $platform
|
||||
| if ($platform | type) == "string" then object_or_empty(.[$platform]?)
|
||||
else {}
|
||||
end);
|
||||
{
|
||||
id: .name,
|
||||
name: .name,
|
||||
version: .version,
|
||||
description: .description,
|
||||
emoji: (platform_meta.emoji // object_field("openclaw").emoji // object_field("hermes").emoji // object_field("nanoclaw").emoji // object_field("picoclaw").emoji // "📦"),
|
||||
category: (platform_meta.category // object_field("openclaw").category // object_field("hermes").category // object_field("nanoclaw").category // object_field("picoclaw").category // "utility"),
|
||||
tag: $tag
|
||||
}
|
||||
' "$SKILL_JSON")
|
||||
|
||||
# Append to index
|
||||
if [ "$FIRST_SKILL" = "true" ]; then
|
||||
|
||||
@@ -101,6 +101,19 @@ export interface SkillChecksums {
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SkillPlatformMetadata {
|
||||
emoji?: string;
|
||||
category?: string;
|
||||
feed_url?: string;
|
||||
requires?: {
|
||||
bins?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
triggers?: string[];
|
||||
internal?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SkillJson {
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -116,13 +129,9 @@ export interface SkillJson {
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
openclaw: {
|
||||
emoji: string;
|
||||
category: string;
|
||||
feed_url?: string;
|
||||
requires?: {
|
||||
bins?: string[];
|
||||
};
|
||||
triggers: string[];
|
||||
};
|
||||
platform?: CorePlatformSlug | (string & {});
|
||||
openclaw?: SkillPlatformMetadata | null;
|
||||
hermes?: SkillPlatformMetadata | null;
|
||||
nanoclaw?: SkillPlatformMetadata | null;
|
||||
picoclaw?: SkillPlatformMetadata | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user