Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ The Skill behaviors panel shows a behavior reading under each SKILL.md match tha
Name the model plainly, as a language model. Never call a reading a review, a scan, a safety call, or a false positive.
The strings live in `layers/registry/app/components/_SkillBehaviors.vue`.

- If the rules fail: `Skill behaviors are unavailable. Run skilld run to check every file.`

- Each match: `SKILL.md:42 Β· Quoted example. {reason}`. `behaviorVerdictLabel` in `shared/behavior-readings.ts` owns the five labels: `Instruction`, `Quoted example`, `Prohibition`, `Documentation`, `Unclear`.
- Panel note, shown when a match has a reading: `A language model read each match that needs approval in its context. Its reading is no guarantee and changes no approval.`
- The skilld CLI approval message adds `(model reading: quoted example. {reason})` to each match, and ends its list with `A language model on skilld.dev wrote each model reading. A reading is no guarantee and changes no approval.` The CLI owns that string, so change it there first.
Expand Down
12 changes: 6 additions & 6 deletions layers/registry/app/components/SkillDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ interface SourceFacts {
capabilityScopes: ('read' | 'write' | 'exec' | 'net')[]
mcpServers: string[]
}
behaviors: SkillBehavior[]
behaviors: SkillBehavior[] | null
}

interface NeighborSkill {
Expand Down Expand Up @@ -191,6 +191,7 @@ const skillFetch = useFetch(
githubUrl: string
description: string | null
license: string | null
licenseSource: 'skill' | 'repository' | null
stars: number
forks: number
pushedAt: string | null
Expand Down Expand Up @@ -446,8 +447,7 @@ const { data: skillFiles } = useFetch(
total: number
} | null>>

// Prefer the live ungh-walked file list when available (catches markdown
// siblings the sync job hasn't registered yet); fall back to data.assets.
// Prefer the stored file inventory endpoint; fall back to detail metadata.
const treeAssets = computed(() => {
if (data.value?.sourceGone)
return []
Expand Down Expand Up @@ -1655,7 +1655,7 @@ useHead(computed(() => ({
v-if="licenseFile"
type="button"
class="skill-chip skill-chip-link"
:title="`License: ${data.license}. Opens ${licenseFile}.`"
:title="`${data.licenseSource === 'repository' ? 'Repository license' : 'License'}: ${data.license}. Opens ${licenseFile}.`"
@click="() => { void resolveAndOpen(licenseFile!) }"
>
<UIcon name="i-lucide-scale" class="size-3.5" aria-hidden="true" />
Expand All @@ -1664,7 +1664,7 @@ useHead(computed(() => ({
<span
v-else
class="skill-chip"
:title="`License: ${data.license}`"
:title="`${data.licenseSource === 'repository' ? 'Repository license' : 'License'}: ${data.license}`"
>
<UIcon name="i-lucide-scale" class="size-3.5" aria-hidden="true" />
{{ licenseLabel }}
Expand Down Expand Up @@ -2335,7 +2335,7 @@ useHead(computed(() => ({
>
<SkillBehaviors
v-if="data.raw || behaviors.length"
:behaviors="behaviors"
:behaviors="data.sourceFacts.behaviors"
:readings="behaviorReadings"
/>
<SkillThirdPartyChecks
Expand Down
2 changes: 1 addition & 1 deletion layers/registry/app/components/SkillFileTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const emit = defineEmits<{

const nodes = computed(() => buildSkillFileTree(props.assets, props.skillMdSize ?? 0))

const skillDir = computed(() => props.skillPath?.replace(/\/SKILL\.md$/, '') ?? '')
const skillDir = computed(() => props.skillPath?.replace(/(?:^|\/)SKILL\.md$/i, '') ?? '')

/**
* The skill page renders this tree twice, once floating and once inline, so a
Expand Down
3 changes: 2 additions & 1 deletion layers/registry/app/components/SkillFileTreeNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const isActive = computed(() => {
const githubUrl = computed(() => {
if (props.node.kind !== 'file')
return ''
return `https://github.com/${props.owner}/${props.repo}/blob/${props.branch}/${props.skillDir}/${props.node.path}`
const path = props.skillDir ? `${props.skillDir}/${props.node.path}` : props.node.path
return `https://github.com/${props.owner}/${props.repo}/blob/${props.branch}/${path}`
})

// A plain click opens the file in the viewer. Modified clicks keep the
Expand Down
8 changes: 4 additions & 4 deletions layers/registry/app/components/_SkillBehaviors.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface SkillBehavior {
}

const { behaviors, readings = [] } = defineProps<{
behaviors: SkillBehavior[]
behaviors: SkillBehavior[] | null
readings?: BehaviorReading[]
}>()

Expand All @@ -45,7 +45,7 @@ function locationRows(behavior: SkillBehavior): Array<{ location: SkillBehaviorL
return behavior.locations.map(location => ({ location, reading: readingFor(behavior, location) }))
}

const anyReading = computed(() => behaviors.some(hasReadings))
const anyReading = computed(() => behaviors?.some(hasReadings) ?? false)

function locationLabel(location: SkillBehaviorLocation): string {
return location.line === null ? location.path : `${location.path}:${location.line}`
Expand All @@ -67,7 +67,7 @@ function locationLabel(location: SkillBehaviorLocation): string {

<div class="rounded-lg border border-default">
<ul
v-if="behaviors.length"
v-if="behaviors?.length"
role="list"
class="divide-y divide-default"
>
Expand Down Expand Up @@ -153,7 +153,7 @@ function locationLabel(location: SkillBehaviorLocation): string {
v-else
class="px-4 py-3 font-mono text-xs text-muted"
>
No rule matched.
{{ behaviors === null ? 'Skill behaviors are unavailable. Run skilld run to check every file.' : 'No rule matched.' }}
</p>
<div class="space-y-1 border-t border-default bg-muted/20 px-4 py-3 text-xs text-muted leading-snug">
<p>skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.</p>
Expand Down
88 changes: 15 additions & 73 deletions layers/registry/server/api/skill-asset/[...slug].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { normalizeSkillAssetFilePath } from '#shared/skill-asset-path'
import { resolveRepoSourceIdentityFromRow } from '../../utils/repo-source-identity'
import { skillImagePolicyForEvent } from '../../utils/skill-image-policy'
import { parseSkillMd } from '../../utils/skill-md-render'
import { loadStoredSkillRow, parseStoredAssets, resolveReferencedFileTarget } from '../../utils/skill-stored-source'
import { findSkill } from '../../utils/skills-registry'
import { fetchUpstreamText } from '../../utils/upstream-text'
import { fetchUpstreamTree } from '../../utils/upstream-tree'

const ASSET_CACHE_TTL = 60 * 60 * 24 * 7
const ASSET_MISSING_TTL = 60 * 60
Expand All @@ -22,14 +22,6 @@ interface AssetCache {
skillPath: string | null
}

interface SkillAssetRow {
default_branch: string | null
assets: string | null
source_owner: string | null
source_repo: string | null
source_resolved: number | null
}

interface RepoSkillNameRow {
name: string
}
Expand Down Expand Up @@ -79,12 +71,7 @@ export default defineApiHandler({
throw createError({ statusCode: 404, message: 'Skill not found' })

const [row, repoSkillRows] = await Promise.all([
platform.db
.prepare(`SELECT r.default_branch, r.source_owner, r.source_repo, s.assets, s.source_resolved
FROM skills s JOIN repos r ON r.owner = s.owner AND r.repo = s.repo
WHERE s.owner = ? AND s.repo = ? AND s.name = ?`)
.bind(skill.owner, skill.repo, skill.name)
.first<SkillAssetRow>(),
loadStoredSkillRow(platform.db, skill),
platform.db
.prepare(`SELECT name FROM skills WHERE owner = ? AND repo = ? ORDER BY name`)
.bind(skill.owner, skill.repo)
Expand All @@ -101,21 +88,7 @@ export default defineApiHandler({
throw createError({ statusCode: 410, message: 'Skill source is gone upstream' })

const source = resolveRepoSourceIdentityFromRow(skill, row)
let registered: RegisteredAsset[] = []
if (row.assets) {
try {
const parsed = JSON.parse(row.assets) as unknown
if (Array.isArray(parsed)) {
registered = parsed.filter((a): a is RegisteredAsset =>
Boolean(a) && typeof a === 'object' && typeof (a as { path: unknown }).path === 'string',
)
}
}
catch {
// Treat as empty list.
}
}

const registered = parseStoredAssets(row.assets)
// Any file inside the resolved skillDir is fair game β€” the path is
// already constrained below so this stays scoped to the skill folder.
// Registration is used only to pick up the recorded size/type when
Expand All @@ -124,56 +97,25 @@ export default defineApiHandler({
const asset: RegisteredAsset = registeredAsset
?? { path: filePath, size: 0, type: classifyAsset(filePath) }

const branch = row.default_branch || 'main'
const cacheKey = `skills:asset:v4:${source.owner}/${source.repo}/${skill.name}:${filePath}:${branch}`
const target = resolveReferencedFileTarget(skill, row, filePath)
if (target._tag === 'missing')
throw createError({ statusCode: 404, message: 'Skill source not found' })
if (target._tag === 'unavailable') {
setHeader(event, 'retry-after', ASSET_RETRY_AFTER)
throw createError({ statusCode: 503, message: 'Skill source commit is unavailable. Try again after the next sync.' })
}
const branch = row.rendered_commit_sha!
const skillMdPath = row.rendered_skill_path!
const skillDir = skillMdPath.replace(/(?:^|\/)SKILL\.md$/i, '')
const cacheKey = `skills:asset:v5:${source.owner}/${source.repo}/${skill.name}:${filePath}:${branch}`
const cached = await readCache<AssetCache>(useStorage('edge-cache'), cacheKey)
if (cached) {
if (cached.status === 'missing')
throw createError({ statusCode: 404, message: 'Asset content unavailable' })
return cached
}

// Resolve the skill directory by re-finding the SKILL.md path.
const treeResult = await fetchUpstreamTree(source, branch, { operation: 'skill-asset-tree-fetch' })

if (treeResult._tag === 'gone') {
// The registry has not recorded this deletion yet. The next sync flips
// `source_resolved` and short-circuits earlier.
throw createError({ statusCode: 410, message: 'Skill source is gone upstream' })
}

const treeUnavailable = treeResult._tag === 'unavailable'
const treeFiles = treeResult._tag === 'available' ? treeResult.files : []
const slugifiedName = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
const skillMdPath = treeFiles.find(f =>
f.path.toLowerCase().endsWith(`/${slugifiedName}/skill.md`)
|| f.path.toLowerCase() === `${slugifiedName}/skill.md`
|| f.path.toLowerCase().endsWith(`/${skill.name.toLowerCase()}/skill.md`),
)?.path

if (!skillMdPath) {
// A tree-listing outage must not leave a "missing" marker behind, or the
// asset reads as deleted for the rest of the cache window.
if (treeUnavailable) {
setHeader(event, 'retry-after', ASSET_RETRY_AFTER)
throw createError({ statusCode: 503, message: 'Skill source is unavailable upstream' })
}
await writeCache(useStorage('edge-cache'), cacheKey, {
status: 'missing',
raw: null,
html: null,
size: asset.size,
type: asset.type,
branch,
skillPath: null,
} satisfies AssetCache, { ttl: ASSET_MISSING_TTL })
throw createError({ statusCode: 404, message: 'Skill source not found' })
}

const skillDir = skillMdPath.replace(/\/SKILL\.md$/, '')
const fullPath = `${skillDir}/${filePath}`
const rawUrl = `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${branch}/${fullPath}`
const upstream = await fetchUpstreamText(rawUrl)
const upstream = await fetchUpstreamText(target.url)

if (upstream._tag === 'missing') {
emitOperationalEvent(createWideEvent({ 'operation': 'skill-asset-fetch', 'outcome': 'missing', 'upstream.status': upstream.status }))
Expand Down
4 changes: 4 additions & 0 deletions layers/registry/server/api/skills-raw/[...slug].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export default defineApiHandler({
// A file beside SKILL.md (e.g. `references/foo.md`) is not stored, so it
// is the one read that still reaches GitHub. Its directory comes from D1.
const target = resolveReferencedFileTarget(skill, row, filePath)
if (target._tag === 'unavailable') {
setHeader(event, 'retry-after', RAW_RETRY_AFTER)
throw createError({ statusCode: 503, message: 'Skill source commit is unavailable. Try again after the next sync.' })
}
if (target._tag === 'missing')
throw createError({ statusCode: 404, message: 'Referenced file not found in repository' })

Expand Down
Loading
Loading