feat(skills): audit scan results and block unroutable skills
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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'}`);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user