Compare commits

...
Author SHA1 Message Date
Vincent Koc 62480531c6 schema: fix capability field indentation in v1 response schema 2026-02-27 11:06:23 -08:00
Vincent Koc be990c8057 test(convex): cover mixed legacy plus openclaw capabilities 2026-02-27 11:06:19 -08:00
Vincent Koc b7482e750c convex: prefer openclaw capability declarations in mixed metadata 2026-02-27 11:06:14 -08:00
Vincent Koc f65d5e84c2 api(v1): type latestVersion clawdis capabilities field 2026-02-27 11:06:09 -08:00
Vincent Koc da1dab3ec8 docs(skill-format): include full openclaw metadata and capability shapes 2026-02-27 09:45:53 -08:00
Vincent Koc e0d4948e98 schema: expand canonical skill capabilities set 2026-02-27 09:14:23 -08:00
Vincent Koc cd1abd2b14 test(convex): cover capability alias and object shapes 2026-02-27 09:14:16 -08:00
Vincent Koc a0fd033cf0 convex: normalize capability aliases and declaration shapes 2026-02-27 09:14:16 -08:00
Vincent Koc 446be4b4d6 fix(schema): avoid capability schema initialization order issue 2026-02-27 09:09:03 -08:00
Vincent Koc 507a4cb413 test(convex): cover clawdis capabilities parsing 2026-02-27 09:08:36 -08:00
Vincent Koc ee51e9a446 feat(api): expose capabilities in skills v1 responses 2026-02-27 09:08:36 -08:00
Vincent Koc 6ff1dbb014 feat(convex): parse and normalize clawdis capabilities 2026-02-27 09:08:36 -08:00
Vincent Koc 0dc7b1ca30 feat(convex): add capability normalization helper 2026-02-27 09:08:36 -08:00
Vincent Koc dd5a6c88cc feat(schema): add capability enum to clawdis metadata 2026-02-27 09:08:36 -08:00
6 changed files with 250 additions and 3 deletions
+9 -1
View File
@@ -45,7 +45,13 @@ type ListSkillsResult = {
version: string
createdAt: number
changelog: string
parsed?: { clawdis?: { os?: string[]; nix?: { plugin?: boolean; systems?: string[] } } }
parsed?: {
clawdis?: {
os?: string[]
nix?: { plugin?: boolean; systems?: string[] }
capabilities?: string[]
}
}
} | null
}>
nextCursor: string | null
@@ -205,6 +211,7 @@ export async function listSkillsV1Handler(ctx: ActionCtx, request: Request) {
version: item.latestVersion.version,
createdAt: item.latestVersion.createdAt,
changelog: item.latestVersion.changelog,
capabilities: item.latestVersion.parsed?.clawdis?.capabilities ?? [],
}
: null,
metadata: item.latestVersion?.parsed?.clawdis
@@ -301,6 +308,7 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request)
version: result.latestVersion.version,
createdAt: result.latestVersion.createdAt,
changelog: result.latestVersion.changelog,
capabilities: result.latestVersion.parsed?.clawdis?.capabilities ?? [],
}
: null,
metadata: result.latestVersion?.parsed?.clawdis
+118
View File
@@ -0,0 +1,118 @@
const SKILL_CAPABILITIES = [
'shell',
'filesystem',
'network',
'browser',
'sessions',
'messaging',
'scheduling',
] as const
export type SkillCapability = (typeof SKILL_CAPABILITIES)[number]
const SKILL_CAPABILITY_SET = new Set<string>(SKILL_CAPABILITIES)
const CAPABILITY_ALIASES: Record<string, SkillCapability> = {
// shell
bash: 'shell',
command: 'shell',
commands: 'shell',
exec: 'shell',
process: 'shell',
shell: 'shell',
terminal: 'shell',
shell_exec: 'shell',
// filesystem
apply_patch: 'filesystem',
edit: 'filesystem',
file: 'filesystem',
files: 'filesystem',
filesystem: 'filesystem',
fs: 'filesystem',
write: 'filesystem',
// network
fetch: 'network',
http: 'network',
mcp: 'network',
network: 'network',
web: 'network',
'web-fetch': 'network',
web_fetch: 'network',
webfetch: 'network',
web_search: 'network',
'network.fetch': 'network',
'network.search': 'network',
// browser
browser: 'browser',
'computer-use': 'browser',
computer_use: 'browser',
gui: 'browser',
screen: 'browser',
ui: 'browser',
// sessions
delegate: 'sessions',
orchestration: 'sessions',
sessions: 'sessions',
sessions_send: 'sessions',
sessions_spawn: 'sessions',
subagent: 'sessions',
subagents: 'sessions',
// messaging
chat: 'messaging',
message: 'messaging',
messages: 'messaging',
messaging: 'messaging',
// scheduling
cron: 'scheduling',
schedule: 'scheduling',
scheduler: 'scheduling',
scheduling: 'scheduling',
timer: 'scheduling',
}
function normalizeCapabilityName(input: string): SkillCapability | null {
const key = input.trim().toLowerCase()
if (!key) return null
if (SKILL_CAPABILITY_SET.has(key)) return key as SkillCapability
const alias = CAPABILITY_ALIASES[key]
if (alias) return alias
const firstSegment = key.split(/[._:-]/)[0]
if (SKILL_CAPABILITY_SET.has(firstSegment)) return firstSegment as SkillCapability
return null
}
function extractCapabilityNames(input: unknown): string[] {
if (!input) return []
if (typeof input === 'string') return [input]
if (Array.isArray(input)) {
return input.flatMap((entry) => {
if (typeof entry === 'string') return [entry]
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return []
const obj = entry as Record<string, unknown>
const named = [obj.name, obj.type, obj.id, obj.capability].find(
(value) => typeof value === 'string',
)
return typeof named === 'string' ? [named] : []
})
}
if (typeof input === 'object') {
return Object.keys(input as Record<string, unknown>)
}
return []
}
export function normalizeCapabilities(input: unknown): SkillCapability[] {
const rawNames = extractCapabilityNames(input)
const out = new Set<SkillCapability>()
for (const rawName of rawNames) {
const normalized = normalizeCapabilityName(rawName)
if (normalized) out.add(normalized)
}
return Array.from(out)
}
+51
View File
@@ -61,6 +61,57 @@ describe('skills utils', () => {
expect(clawdis?.requires?.bins).toEqual(['rg'])
})
it('parses capabilities from clawdis metadata', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"clawdis":{"capabilities":["shell","network","unknown"]}}\n---\nBody`,
)
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.capabilities).toEqual(['shell', 'network'])
})
it('normalizes alias capability names and new canonical values', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"clawdis":{"capabilities":["terminal","web_fetch","subagent","cron","message"]}}\n---\nBody`,
)
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.capabilities).toEqual([
'shell',
'network',
'sessions',
'scheduling',
'messaging',
])
})
it('accepts object-style capabilities with inline constraints', () => {
const frontmatter = parseFrontmatter(`---
metadata:
clawdis:
capabilities:
shell:
mode: restricted
allow: [git, gh]
network:
web_search: true
web_fetch: true
---`)
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.capabilities).toEqual(['shell', 'network'])
})
it('prefers openclaw capabilities when mixed with legacy clawdis metadata', () => {
const frontmatter = parseFrontmatter(`---
metadata:
clawdis:
emoji: "🦞"
openclaw:
capabilities: [terminal, web_fetch]
---`)
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.emoji).toBe('🦞')
expect(clawdis?.capabilities).toEqual(['shell', 'network'])
})
it('ignores invalid clawdis metadata', () => {
const frontmatter = parseFrontmatter(`---\nmetadata: not-json\n---\nBody`)
expect(parseClawdisMetadata(frontmatter)).toBeUndefined()
+10 -2
View File
@@ -9,6 +9,7 @@ import {
TEXT_FILE_EXTENSION_SET,
} from 'clawhub-schema'
import { parse as parseYaml } from 'yaml'
import { normalizeCapabilities } from './skillCapabilities'
export type ParsedSkillFrontmatter = Record<string, unknown>
export type { ClawdisSkillMetadata, SkillInstallSpec }
@@ -70,13 +71,17 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
const clawdbotMeta = metadataRecord?.clawdbot
const clawdisMeta = metadataRecord?.clawdis
const openclawMeta = metadataRecord?.openclaw
const openclawObj =
openclawMeta && typeof openclawMeta === 'object' && !Array.isArray(openclawMeta)
? (openclawMeta as Record<string, unknown>)
: undefined
const metadataSource =
clawdbotMeta && typeof clawdbotMeta === 'object' && !Array.isArray(clawdbotMeta)
? (clawdbotMeta as Record<string, unknown>)
: clawdisMeta && typeof clawdisMeta === 'object' && !Array.isArray(clawdisMeta)
? (clawdisMeta as Record<string, unknown>)
: openclawMeta && typeof openclawMeta === 'object' && !Array.isArray(openclawMeta)
? (openclawMeta as Record<string, unknown>)
: openclawObj
? openclawObj
: undefined
const clawdisRaw = metadataSource ?? frontmatter.clawdis
@@ -139,6 +144,9 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
if (typeof clawdisObj.author === 'string') metadata.author = clawdisObj.author
const links = parseSkillLinks(clawdisObj.links)
if (links) metadata.links = links
// Prefer canonical openclaw capability declarations when present, even in mixed legacy manifests.
const capabilities = normalizeCapabilities(openclawObj?.capabilities ?? clawdisObj.capabilities)
if (capabilities.length > 0) metadata.capabilities = capabilities
return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata')
} catch {
+54
View File
@@ -82,9 +82,63 @@ metadata:
| `emoji` | `string` | Display emoji for the skill. |
| `homepage` | `string` | URL to the skill's homepage or docs. |
| `os` | `string[]` | OS restrictions (e.g. `["macos"]`, `["linux"]`). |
| `capabilities` | `string[] \| object \| object[]` | Capability declarations (`shell`, `filesystem`, `network`, `browser`, `sessions`, `messaging`, `scheduling`). |
| `install` | `array` | Install specs for dependencies (see below). |
| `nix` | `object` | Nix plugin spec (see README). |
| `config` | `object` | Clawdbot config spec (see README). |
| `cliHelp` | `string` | Optional CLI help text shown in skill details. |
| `envVars` | `array` | Structured env var declarations (`name`, `required`, `description`). |
| `dependencies` | `array` | Structured dependency declarations (`name`, `type`, optional metadata). |
| `author` | `string` | Optional skill author string. |
| `links` | `object` | Optional links (`homepage`, `repository`, `documentation`, `changelog`). |
### Capabilities shape and normalization
`metadata.openclaw.capabilities` supports flat and 2-layer shapes under the same key.
Flat list:
```yaml
metadata:
openclaw:
capabilities: [shell, network, sessions]
```
2-layer object (constraints as key/value pairs):
```yaml
metadata:
openclaw:
capabilities:
shell:
mode: restricted
allow: [git, gh]
network:
web_search: true
web_fetch: true
```
Array-of-objects is also accepted:
```yaml
metadata:
openclaw:
capabilities:
- type: network.search
constraints:
provider: brave
- name: shell.exec
constraints:
mode: restricted
```
Aliases are normalized to canonicals at parse time:
- `web_fetch`, `web_search`, `webfetch` -> `network`
- `terminal`, `bash`, `exec` -> `shell`
- `subagent`, `sessions_spawn` -> `sessions`
- `message` -> `messaging`
- `cron`, `schedule` -> `scheduling`
### Install specs
+8
View File
@@ -171,6 +171,7 @@ export const ApiV1SkillListResponseSchema = type({
version: 'string',
createdAt: 'number',
changelog: 'string',
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
}).optional(),
}).array(),
nextCursor: 'string|null',
@@ -190,6 +191,7 @@ export const ApiV1SkillResponseSchema = type({
version: 'string',
createdAt: 'number',
changelog: 'string',
capabilities: '("shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling")[]?',
}).or('null'),
owner: type({
handle: 'string|null',
@@ -295,6 +297,11 @@ export const ClawdisRequiresSchema = type({
})
export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]
export const SkillCapabilitySchema = type(
'"shell"|"filesystem"|"network"|"browser"|"sessions"|"messaging"|"scheduling"',
)
export type SkillCapability = (typeof SkillCapabilitySchema)[inferred]
export const EnvVarDeclarationSchema = type({
name: 'string',
required: 'boolean?',
@@ -335,5 +342,6 @@ export const ClawdisSkillMetadataSchema = type({
dependencies: DependencyDeclarationSchema.array().optional(),
author: 'string?',
links: SkillLinksSchema.optional(),
capabilities: SkillCapabilitySchema.array().optional(),
})
export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]