diff --git a/src/audit/logger.ts b/src/audit/logger.ts index b08bf16..663f9c3 100644 --- a/src/audit/logger.ts +++ b/src/audit/logger.ts @@ -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, + }); + } + // ── Session Events ─────────────────────────────────────────── sessionCreate(event: SessionCreateEvent): void { diff --git a/src/audit/types.ts b/src/audit/types.ts index a66b6c2..2e63288 100644 --- a/src/audit/types.ts +++ b/src/audit/types.ts @@ -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; diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index 8ed0360..d97f02d 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -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}`; diff --git a/src/skills/installer.ts b/src/skills/installer.ts index ff09b3f..3660708 100644 --- a/src/skills/installer.ts +++ b/src/skills/installer.ts @@ -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'}`); diff --git a/src/skills/loader.ts b/src/skills/loader.ts index b8d4be1..086df61 100644 --- a/src/skills/loader.ts +++ b/src/skills/loader.ts @@ -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 => {