feat(skills): audit scan results and block unroutable skills

This commit is contained in:
William Valentin
2026-02-15 11:06:52 -08:00
parent 83752d4e1c
commit 56e887a6bf
5 changed files with 50 additions and 1 deletions
+11
View File
@@ -10,6 +10,7 @@ import type {
ToolApprovalEvent,
SkillsInstallerExecutionBlockedEvent,
SkillsInstallerCommandResultEvent,
SkillsScanEvent,
SessionCreateEvent,
SessionMessageEvent,
SessionDeleteEvent,
@@ -113,6 +114,16 @@ export class AuditLogger {
});
}
skillsScan(event: SkillsScanEvent): void {
const level = event.ok ? 'debug' : 'warn';
if (!this.shouldLog('tools', level)) {return;}
this.write({
level,
event_type: event.ok ? 'skills.scan.pass' : 'skills.scan.fail',
event: event as unknown as Record<string, unknown>,
});
}
// ── Session Events ───────────────────────────────────────────
sessionCreate(event: SessionCreateEvent): void {
+12
View File
@@ -3,6 +3,8 @@ export type AuditLevel = 'debug' | 'info' | 'warn' | 'error';
export type AuditEventType =
// Tool execution
| 'tool.start' | 'tool.success' | 'tool.error' | 'tool.denied' | 'tool.approval'
// Skills scan
| 'skills.scan.pass' | 'skills.scan.fail'
// Skills installer
| 'skills.installer.execution_blocked' | 'skills.installer.command_result'
// Session lifecycle
@@ -122,6 +124,16 @@ export interface SkillsInstallerCommandResultEvent {
reason: string;
}
export interface SkillsScanEvent {
skill_name: string;
tier: 'bundled' | 'managed' | 'workspace' | 'unknown';
phase: 'load' | 'install';
ok: boolean;
error_count: number;
warn_count: number;
issue_codes: string[];
}
export interface SessionCreateEvent {
session_id: string;
frontend: string;
+7 -1
View File
@@ -78,7 +78,13 @@ export function createMessageRouter(deps: {
const tierFromMetadata = metadata?.modelTier as ModelTier | undefined;
// Include agent config name in cache key so different agents aren't shared
const skillOverride = metadata?.skillOverride as string | undefined;
let skillOverride = metadata?.skillOverride as string | undefined;
if (skillOverride && deps.skillRegistry) {
const s = deps.skillRegistry.get(skillOverride);
if (!s || !s.available) {
skillOverride = undefined;
}
}
const baseSid = agentConfigName || skillOverride
? `${channel}:${senderId}:${agentConfigName ?? 'default'}:${skillOverride ?? 'none'}`
: `${channel}:${senderId}`;
+10
View File
@@ -3,6 +3,7 @@ import { resolve, basename } from 'path';
import type { Skill } from './types.js';
import { loadSkill } from './loader.js';
import { scanSkillDirectory } from './scanner.js';
import { auditLogger } from '../audit/index.js';
/**
* SkillInstaller manages installing and removing skills in the managed
@@ -39,6 +40,15 @@ export class SkillInstaller {
}
const scan = scanSkillDirectory(sourceDir);
auditLogger?.skillsScan({
skill_name: basename(sourceDir),
tier: 'unknown',
phase: 'install',
ok: scan.ok,
error_count: scan.issues.filter(i => i.severity === 'error').length,
warn_count: scan.issues.filter(i => i.severity === 'warn').length,
issue_codes: Array.from(new Set(scan.issues.map(i => i.code))),
});
if (!scan.ok) {
const codes = Array.from(new Set(scan.issues.map(i => i.code))).join(', ');
throw new Error(`Skill scan failed: ${codes || 'unknown_issue'}`);
+10
View File
@@ -11,6 +11,7 @@ import { execSync } from 'child_process';
import { platform } from 'os';
import type { Skill, SkillManifest, SkillRequirements, SkillTier } from './types.js';
import { scanSkillDirectory } from './scanner.js';
import { auditLogger } from '../audit/index.js';
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
@@ -200,6 +201,15 @@ export function loadSkill(directory: string, tier: SkillTier): Skill | null {
const instructions = readFileSync(instructionsPath, 'utf-8');
const scan = scanSkillDirectory(absDir);
auditLogger?.skillsScan({
skill_name: basename(absDir),
tier,
phase: 'load',
ok: scan.ok,
error_count: scan.issues.filter(i => i.severity === 'error').length,
warn_count: scan.issues.filter(i => i.severity === 'warn').length,
issue_codes: Array.from(new Set(scan.issues.map(i => i.code))),
});
const scanReasons = scan.issues.map((i) => `${i.code}: ${i.message}${i.path ? ` (${basename(i.path)})` : ''}`);
const inferManifest = (): SkillManifest => {