blob: 83674a4a3ea738de021dbc7b6cdcf7bf8d9b6236 [file]
import { requireCount, requireEntityId, requireExactRecord, requireRecord } from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
export const SKILL_CATALOG_PAGE_MAX_ITEMS = 128;
export const SKILL_CATALOG_PAGE_MAX_BYTES = 48 * 1024;
export const SKILL_CATALOG_PREVIEW_RESULT_MAX_BYTES = 48 * 1024;
export const SKILL_CATALOG_REF_MAX_BYTES = 512;
export const SKILL_CATALOG_DISPLAY_ID_MAX_BYTES = 256;
const PROJECT_ROOT_MAX_BYTES = 4096;
const CURSOR_MAX_BYTES = 1024;
const ID_MAX_BYTES = 81;
export const SKILL_CATALOG_NAME_MAX_BYTES = 256;
export const SKILL_CATALOG_DESCRIPTION_MAX_BYTES = 4096;
export const SKILL_CATALOG_CATEGORY_MAX_BYTES = 128;
export const SKILL_CATALOG_STRING_ARRAY_MAX_ITEMS = 64;
export const SKILL_CATALOG_STRING_ARRAY_ITEM_MAX_BYTES = 256;
const PREVIEW_SNIPPET_MAX_BYTES = 24 * 1024;
const QUERY_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'invalid_request',
'persistence_failed',
'internal_failure',
] as const;
const MUTATION_ERRORS = [...QUERY_ERRORS, 'commit_outcome_unknown'] as const;
export type SkillCatalogRevision = `sha256:${string}`;
export type SkillContentSha256 = SkillCatalogRevision;
export type SkillCatalogView = 'governance' | 'bundled' | 'managed_sources';
export type SkillCatalogEntryKind = 'skill' | 'discovery_diagnostic';
export type SkillCatalogSourceType = 'workspace' | 'bundled' | 'managed' | 'unknown';
export type SkillCatalogValidationStatus = 'ok' | 'missing_lock' | 'modified' | 'metadata_error';
export type SkillCatalogManagedUpdateStatus =
| 'not_managed'
| 'source_missing'
| 'up_to_date'
| 'update_available'
| 'local_modified'
| 'metadata_error';
export type SkillCatalogRuntimeStatus = 'enabled' | 'disabled' | 'state_error';
export type SkillCatalogScope = 'project' | 'workspace' | 'user' | 'custom';
export type SkillCatalogDiscoverySource = 'maka' | 'agents' | 'legacy' | 'custom';
export type SkillCatalogContextStatus =
| 'unknown'
| 'advertised'
| 'disabled'
| 'invalid'
| 'host_incompatible'
| 'shadowed'
| 'budget';
export type SkillCatalogValidationCode =
| 'missing_lock'
| 'modified'
| 'invalid_json'
| 'id_mismatch'
| 'unsupported_schema'
| 'invalid_hash'
| 'write_failed'
| 'lock_symlink'
| 'missing_frontmatter'
| 'malformed_frontmatter'
| 'missing_name'
| 'invalid_name'
| 'name_too_long'
| 'missing_description'
| 'invalid_description'
| 'description_too_long'
| 'invalid_allowed_tools'
| 'invalid_required_tools'
| 'invalid_required_capabilities'
| 'invalid_license'
| 'invalid_compatibility'
| 'compatibility_too_long'
| 'invalid_metadata'
| 'invalid_category'
| 'unsupported_field'
| 'body_too_large'
| 'duplicate_id'
| 'duplicate_name'
| 'blocked_path'
| 'read_failed'
| 'projection_truncated';
export interface SkillCatalogLocalContext {
readonly projectRoot: string;
}
export type SkillCatalogInvocableTarget =
| { readonly kind: 'session'; readonly sessionId: string }
| {
readonly kind: 'new_session';
readonly context: SkillCatalogLocalContext;
readonly collaborationMode: 'agent' | 'plan';
};
export interface SkillCatalogInvocableItem {
readonly ref: string;
readonly id: string;
readonly name: string;
readonly description: string;
}
export interface SkillCatalogGovernanceItem {
readonly kind: SkillCatalogEntryKind;
readonly ref: string;
readonly id: string;
readonly name: string;
readonly description: string;
readonly declaredTools: readonly string[];
readonly metadataTruncated: boolean;
readonly sourceType: SkillCatalogSourceType;
readonly userModified: boolean;
readonly validationStatus: SkillCatalogValidationStatus;
readonly validationCodes: readonly SkillCatalogValidationCode[];
readonly managedUpdateStatus: SkillCatalogManagedUpdateStatus | null;
readonly enabled: boolean;
readonly pinned: boolean;
readonly runtimeStatus: SkillCatalogRuntimeStatus;
readonly scope: SkillCatalogScope;
readonly source: SkillCatalogDiscoverySource;
readonly contextStatus: SkillCatalogContextStatus;
readonly contextRank: number | null;
readonly shadowedBy: string | null;
readonly needsReview: boolean;
readonly manageable: boolean;
}
export interface SkillCatalogBundledItem {
readonly kind: 'bundled';
readonly id: string;
readonly name: string;
readonly description: string;
readonly category: string;
readonly declaredTools: readonly string[];
readonly metadataTruncated: boolean;
readonly installed: boolean;
}
export interface SkillCatalogManagedSourceItem {
readonly kind: 'managed_source';
readonly id: string;
readonly name: string;
readonly description: string;
readonly category: string;
readonly sourceType: 'local';
readonly metadataTruncated: boolean;
readonly installed: boolean;
}
export type SkillCatalogPageItem =
| SkillCatalogGovernanceItem
| SkillCatalogBundledItem
| SkillCatalogManagedSourceItem;
export type SkillCatalogQueryInput =
| {
readonly kind: 'start';
readonly context: SkillCatalogLocalContext;
readonly view: SkillCatalogView;
}
| {
readonly kind: 'continue';
readonly context: SkillCatalogLocalContext;
readonly view: SkillCatalogView;
readonly revision: SkillCatalogRevision;
readonly cursor: string;
};
export type SkillCatalogQueryResult =
| {
readonly kind: 'page';
readonly view: SkillCatalogView;
readonly revision: SkillCatalogRevision;
readonly items: readonly SkillCatalogPageItem[];
readonly nextCursor: string | null;
}
| {
readonly kind: 'revision_changed';
readonly expectedRevision: SkillCatalogRevision;
readonly actualRevision: SkillCatalogRevision;
};
export type SkillCatalogInvocableQueryInput =
| {
readonly kind: 'start';
readonly target: SkillCatalogInvocableTarget;
}
| {
readonly kind: 'continue';
readonly target: SkillCatalogInvocableTarget;
readonly revision: SkillCatalogRevision;
readonly cursor: string;
};
export type SkillCatalogInvocableQueryResult =
| {
readonly kind: 'page';
readonly revision: SkillCatalogRevision;
readonly items: readonly SkillCatalogInvocableItem[];
readonly nextCursor: string | null;
}
| {
readonly kind: 'revision_changed';
readonly expectedRevision: SkillCatalogRevision;
readonly actualRevision: SkillCatalogRevision;
};
export type SkillCatalogMutation =
| { readonly kind: 'create_starter' }
| {
readonly kind: 'install';
readonly sourceType: 'bundled' | 'managed';
readonly sourceId: string;
}
| SkillCatalogManagedUpdateMutation
| { readonly kind: 'delete'; readonly ref: string }
| { readonly kind: 'set_enabled'; readonly ref: string; readonly enabled: boolean }
| { readonly kind: 'set_pinned'; readonly ref: string; readonly pinned: boolean };
export type SkillCatalogManagedUpdateMutation =
| {
readonly kind: 'update_managed';
readonly ref: string;
readonly force: false;
readonly expectedCurrentSha256: null;
readonly expectedSourceSha256: null;
}
| {
readonly kind: 'update_managed';
readonly ref: string;
readonly force: true;
readonly expectedCurrentSha256: SkillContentSha256;
readonly expectedSourceSha256: SkillContentSha256;
};
export interface SkillCatalogMutateInput {
readonly context: SkillCatalogLocalContext;
readonly expectedRevision: SkillCatalogRevision;
readonly mutation: SkillCatalogMutation;
}
export type SkillCatalogMutationRejectedReason =
| 'not_found'
| 'already_exists'
| 'blocked_scope'
| 'not_managed'
| 'source_missing'
| 'source_changed'
| 'source_invalid'
| 'local_modified'
| 'metadata_error'
| 'needs_review'
| 'blocked_path'
| 'state_error';
export type SkillCatalogMutateResult =
| {
readonly kind: 'committed' | 'unchanged';
readonly revision: SkillCatalogRevision;
readonly entry: SkillCatalogGovernanceItem | null;
}
| SkillCatalogRevisionConflict
| { readonly kind: 'rejected'; readonly reason: SkillCatalogMutationRejectedReason };
export interface SkillCatalogPreviewUpdateInput {
readonly context: SkillCatalogLocalContext;
readonly expectedRevision: SkillCatalogRevision;
readonly ref: string;
}
export interface SkillCatalogPreviewLineSummary {
readonly currentLineCount: number;
readonly sourceLineCount: number;
readonly changedLineCount: number;
}
export type SkillCatalogPreviewRejectedReason =
| 'not_found'
| 'not_managed'
| 'source_missing'
| 'source_invalid'
| 'metadata_error';
export type SkillCatalogPreviewUpdateResult =
| {
readonly kind: 'preview';
readonly revision: SkillCatalogRevision;
readonly currentSnippet: string;
readonly sourceSnippet: string;
readonly currentTruncated: boolean;
readonly sourceTruncated: boolean;
readonly hasManagedBaseline: boolean;
readonly summary: SkillCatalogPreviewLineSummary;
readonly expectedCurrentSha256: SkillContentSha256;
readonly expectedSourceSha256: SkillContentSha256;
}
| SkillCatalogRevisionConflict
| { readonly kind: 'rejected'; readonly reason: SkillCatalogPreviewRejectedReason };
export interface SkillCatalogRevisionConflict {
readonly kind: 'revision_conflict';
readonly expectedRevision: SkillCatalogRevision;
readonly actualRevision: SkillCatalogRevision;
}
export const SKILL_CATALOG_OPERATION_SPECS = {
'skill.catalog.query': defineOperation<
SkillCatalogQueryInput,
SkillCatalogQueryResult,
(typeof QUERY_ERRORS)[number]
>({
mode: 'query',
availability: 'ready',
errors: QUERY_ERRORS,
decodeInput: decodeQueryInput,
decodeOutput: decodeQueryResult,
}),
'skill.catalog.invocable.query': defineOperation<
SkillCatalogInvocableQueryInput,
SkillCatalogInvocableQueryResult,
(typeof QUERY_ERRORS)[number]
>({
mode: 'query',
availability: 'ready',
errors: QUERY_ERRORS,
decodeInput: decodeInvocableQueryInput,
decodeOutput: decodeInvocableQueryResult,
}),
'skill.catalog.mutate': defineOperation<
SkillCatalogMutateInput,
SkillCatalogMutateResult,
(typeof MUTATION_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: MUTATION_ERRORS,
decodeInput: decodeMutateInput,
decodeOutput: decodeMutateResult,
}),
'skill.catalog.preview-update': defineOperation<
SkillCatalogPreviewUpdateInput,
SkillCatalogPreviewUpdateResult,
(typeof QUERY_ERRORS)[number]
>({
mode: 'query',
availability: 'ready',
errors: QUERY_ERRORS,
decodeInput: decodePreviewInput,
decodeOutput: decodePreviewResult,
}),
} as const;
function decodeInvocableQueryInput(value: unknown): SkillCatalogInvocableQueryInput {
const record = requireRecord(value, 'invocable skill catalog query input');
if (record.kind === 'start') {
const start = requireExactRecord(record, 'invocable skill catalog start query', [
'kind',
'target',
]);
return { kind: 'start', target: invocableTarget(start.target) };
}
if (record.kind === 'continue') {
const continuation = requireExactRecord(record, 'invocable skill catalog continuation query', [
'kind',
'target',
'revision',
'cursor',
]);
return {
kind: 'continue',
target: invocableTarget(continuation.target),
revision: sha256(continuation.revision, 'invocable skill catalog revision'),
cursor: utf8String(continuation.cursor, 'invocable skill catalog cursor', CURSOR_MAX_BYTES),
};
}
throw invalidProtocolFrame('Invalid invocable skill catalog query kind');
}
function decodeInvocableQueryResult(value: unknown): SkillCatalogInvocableQueryResult {
const record = requireRecord(value, 'invocable skill catalog query result');
if (record.kind === 'revision_changed') return revisionChanged(record);
const page = requireExactRecord(record, 'invocable skill catalog page result', [
'kind',
'revision',
'items',
'nextCursor',
]);
if (page.kind !== 'page' || !Array.isArray(page.items)) {
throw invalidProtocolFrame('Invalid invocable skill catalog page result');
}
if (page.items.length > SKILL_CATALOG_PAGE_MAX_ITEMS) {
throw invalidProtocolFrame('Invocable skill catalog page exceeds item limit');
}
const decoded: SkillCatalogInvocableQueryResult = {
kind: 'page',
revision: sha256(page.revision, 'invocable skill catalog revision'),
items: page.items.map(invocableItem),
nextCursor:
page.nextCursor === null
? null
: utf8String(page.nextCursor, 'invocable skill catalog next cursor', CURSOR_MAX_BYTES),
};
assertJsonByteLimit(decoded, SKILL_CATALOG_PAGE_MAX_BYTES, 'Invocable skill catalog page');
return decoded;
}
function decodeQueryInput(value: unknown): SkillCatalogQueryInput {
const record = requireRecord(value, 'skill catalog query input');
if (record.kind === 'start') {
const start = requireExactRecord(record, 'skill catalog start query', [
'kind',
'context',
'view',
]);
return { kind: 'start', context: localContext(start.context), view: catalogView(start.view) };
}
if (record.kind === 'continue') {
const continuation = requireExactRecord(record, 'skill catalog continuation query', [
'kind',
'context',
'view',
'revision',
'cursor',
]);
return {
kind: 'continue',
context: localContext(continuation.context),
view: catalogView(continuation.view),
revision: sha256(continuation.revision, 'skill catalog revision'),
cursor: utf8String(continuation.cursor, 'skill catalog cursor', CURSOR_MAX_BYTES),
};
}
throw invalidProtocolFrame('Invalid skill catalog query kind');
}
function decodeQueryResult(value: unknown): SkillCatalogQueryResult {
const record = requireRecord(value, 'skill catalog query result');
if (record.kind === 'revision_changed') return revisionChanged(record);
const page = requireExactRecord(record, 'skill catalog page result', [
'kind',
'view',
'revision',
'items',
'nextCursor',
]);
if (page.kind !== 'page' || !Array.isArray(page.items)) {
throw invalidProtocolFrame('Invalid skill catalog page result');
}
if (page.items.length > SKILL_CATALOG_PAGE_MAX_ITEMS) {
throw invalidProtocolFrame('Skill catalog page exceeds item limit');
}
const view = catalogView(page.view);
const decoded: SkillCatalogQueryResult = {
kind: 'page',
view,
revision: sha256(page.revision, 'skill catalog revision'),
items: page.items.map((item) => pageItem(item, view)),
nextCursor:
page.nextCursor === null
? null
: utf8String(page.nextCursor, 'skill catalog next cursor', CURSOR_MAX_BYTES),
};
assertJsonByteLimit(decoded, SKILL_CATALOG_PAGE_MAX_BYTES, 'Skill catalog page');
return decoded;
}
function pageItem(value: unknown, view: SkillCatalogView): SkillCatalogPageItem {
if (view === 'governance') return governanceItem(value);
if (view === 'bundled') return bundledItem(value);
return managedSourceItem(value);
}
function governanceItem(value: unknown): SkillCatalogGovernanceItem {
const record = requireExactRecord(value, 'skill catalog governance item', [
'kind',
'ref',
'id',
'name',
'description',
'declaredTools',
'metadataTruncated',
'sourceType',
'userModified',
'validationStatus',
'validationCodes',
'managedUpdateStatus',
'enabled',
'pinned',
'runtimeStatus',
'scope',
'source',
'contextStatus',
'contextRank',
'shadowedBy',
'needsReview',
'manageable',
]);
return {
kind: entryKind(record.kind),
ref: ref(record.ref),
id: displayId(record.id),
name: utf8String(record.name, 'skill name', SKILL_CATALOG_NAME_MAX_BYTES, true),
description: utf8String(
record.description,
'skill description',
SKILL_CATALOG_DESCRIPTION_MAX_BYTES,
true,
),
declaredTools: stringArray(record.declaredTools, 'declared tools'),
metadataTruncated: bool(record.metadataTruncated, 'skill metadata truncated'),
sourceType: sourceType(record.sourceType),
userModified: bool(record.userModified, 'skill user modified'),
validationStatus: validationStatus(record.validationStatus),
validationCodes: validationCodes(record.validationCodes),
managedUpdateStatus:
record.managedUpdateStatus === null ? null : managedUpdateStatus(record.managedUpdateStatus),
enabled: bool(record.enabled, 'skill enabled'),
pinned: bool(record.pinned, 'skill pinned'),
runtimeStatus: runtimeStatus(record.runtimeStatus),
scope: scope(record.scope),
source: discoverySource(record.source),
contextStatus: contextStatus(record.contextStatus),
contextRank:
record.contextRank === null ? null : positiveCount(record.contextRank, 'skill context rank'),
shadowedBy: record.shadowedBy === null ? null : ref(record.shadowedBy),
needsReview: bool(record.needsReview, 'skill needs review'),
manageable: bool(record.manageable, 'skill manageable'),
};
}
function bundledItem(value: unknown): SkillCatalogBundledItem {
const record = requireExactRecord(value, 'skill catalog bundled item', [
'kind',
'id',
'name',
'description',
'category',
'declaredTools',
'metadataTruncated',
'installed',
]);
if (record.kind !== 'bundled') throw invalidProtocolFrame('Invalid bundled skill kind');
return {
kind: 'bundled',
id: id(record.id, 'bundled skill id'),
name: utf8String(record.name, 'bundled skill name', SKILL_CATALOG_NAME_MAX_BYTES),
description: utf8String(
record.description,
'bundled skill description',
SKILL_CATALOG_DESCRIPTION_MAX_BYTES,
true,
),
category: utf8String(
record.category,
'bundled skill category',
SKILL_CATALOG_CATEGORY_MAX_BYTES,
),
declaredTools: stringArray(record.declaredTools, 'bundled skill declared tools'),
metadataTruncated: bool(record.metadataTruncated, 'bundled skill metadata truncated'),
installed: bool(record.installed, 'bundled skill installed'),
};
}
function managedSourceItem(value: unknown): SkillCatalogManagedSourceItem {
const record = requireExactRecord(value, 'skill catalog managed source item', [
'kind',
'id',
'name',
'description',
'category',
'sourceType',
'metadataTruncated',
'installed',
]);
if (record.kind !== 'managed_source' || record.sourceType !== 'local') {
throw invalidProtocolFrame('Invalid managed skill source item');
}
return {
kind: 'managed_source',
id: id(record.id, 'managed skill source id'),
name: utf8String(record.name, 'managed skill source name', SKILL_CATALOG_NAME_MAX_BYTES),
description: utf8String(
record.description,
'managed skill source description',
SKILL_CATALOG_DESCRIPTION_MAX_BYTES,
true,
),
category: utf8String(
record.category,
'managed skill source category',
SKILL_CATALOG_CATEGORY_MAX_BYTES,
),
sourceType: 'local',
metadataTruncated: bool(record.metadataTruncated, 'managed skill source metadata truncated'),
installed: bool(record.installed, 'managed skill source installed'),
};
}
function decodeMutateInput(value: unknown): SkillCatalogMutateInput {
const record = requireExactRecord(value, 'skill catalog mutate input', [
'context',
'expectedRevision',
'mutation',
]);
return {
context: localContext(record.context),
expectedRevision: sha256(record.expectedRevision, 'expected skill catalog revision'),
mutation: mutation(record.mutation),
};
}
function mutation(value: unknown): SkillCatalogMutation {
const record = requireRecord(value, 'skill catalog mutation');
if (record.kind === 'create_starter') {
requireExactRecord(record, 'create starter skill mutation', ['kind']);
return { kind: 'create_starter' };
}
if (record.kind === 'install') {
const item = requireExactRecord(record, 'install skill mutation', [
'kind',
'sourceType',
'sourceId',
]);
return {
kind: 'install',
sourceType: mutableSourceType(item.sourceType),
sourceId: id(item.sourceId, 'skill source id'),
};
}
if (record.kind === 'update_managed') {
const item = requireExactRecord(record, 'update managed skill mutation', [
'kind',
'ref',
'force',
'expectedCurrentSha256',
'expectedSourceSha256',
]);
const force = bool(item.force, 'force managed skill update');
const expectedCurrentSha256 = nullableSha256(
item.expectedCurrentSha256,
'expected current sha256',
);
const expectedSourceSha256 = nullableSha256(
item.expectedSourceSha256,
'expected source sha256',
);
if (force) {
if (expectedCurrentSha256 === null || expectedSourceSha256 === null) {
throw invalidProtocolFrame('Invalid managed skill update preview confirmation');
}
return {
kind: 'update_managed',
ref: ref(item.ref),
force: true,
expectedCurrentSha256,
expectedSourceSha256,
};
}
if (expectedCurrentSha256 !== null || expectedSourceSha256 !== null) {
throw invalidProtocolFrame('Invalid managed skill update preview confirmation');
}
return {
kind: 'update_managed',
ref: ref(item.ref),
force: false,
expectedCurrentSha256: null,
expectedSourceSha256: null,
};
}
if (record.kind === 'delete') {
const item = requireExactRecord(record, 'delete skill mutation', ['kind', 'ref']);
return { kind: 'delete', ref: ref(item.ref) };
}
if (record.kind === 'set_enabled') {
const item = requireExactRecord(record, 'set skill enabled mutation', [
'kind',
'ref',
'enabled',
]);
return {
kind: 'set_enabled',
ref: ref(item.ref),
enabled: bool(item.enabled, 'skill enabled'),
};
}
if (record.kind === 'set_pinned') {
const item = requireExactRecord(record, 'set skill pinned mutation', ['kind', 'ref', 'pinned']);
return {
kind: 'set_pinned',
ref: ref(item.ref),
pinned: bool(item.pinned, 'skill pinned'),
};
}
throw invalidProtocolFrame('Invalid skill catalog mutation kind');
}
function decodeMutateResult(value: unknown): SkillCatalogMutateResult {
const record = requireRecord(value, 'skill catalog mutate result');
if (record.kind === 'revision_conflict') return revisionConflict(record);
if (record.kind === 'rejected') {
const rejected = requireExactRecord(record, 'skill catalog mutation rejected result', [
'kind',
'reason',
]);
return { kind: 'rejected', reason: mutationRejectedReason(rejected.reason) };
}
const result = requireExactRecord(record, 'skill catalog mutation result', [
'kind',
'revision',
'entry',
]);
if (result.kind !== 'committed' && result.kind !== 'unchanged') {
throw invalidProtocolFrame('Invalid skill catalog mutation result kind');
}
return {
kind: result.kind,
revision: sha256(result.revision, 'skill catalog revision'),
entry: result.entry === null ? null : governanceItem(result.entry),
};
}
function decodePreviewInput(value: unknown): SkillCatalogPreviewUpdateInput {
const record = requireExactRecord(value, 'skill catalog preview input', [
'context',
'expectedRevision',
'ref',
]);
return {
context: localContext(record.context),
expectedRevision: sha256(record.expectedRevision, 'expected skill catalog revision'),
ref: ref(record.ref),
};
}
function decodePreviewResult(value: unknown): SkillCatalogPreviewUpdateResult {
const record = requireRecord(value, 'skill catalog preview result');
if (record.kind === 'revision_conflict') return revisionConflict(record);
if (record.kind === 'rejected') {
const rejected = requireExactRecord(record, 'skill catalog preview rejected result', [
'kind',
'reason',
]);
return { kind: 'rejected', reason: previewRejectedReason(rejected.reason) };
}
const preview = requireExactRecord(record, 'skill catalog preview result', [
'kind',
'revision',
'currentSnippet',
'sourceSnippet',
'currentTruncated',
'sourceTruncated',
'hasManagedBaseline',
'summary',
'expectedCurrentSha256',
'expectedSourceSha256',
]);
if (preview.kind !== 'preview') throw invalidProtocolFrame('Invalid skill catalog preview kind');
const decoded: SkillCatalogPreviewUpdateResult = {
kind: 'preview',
revision: sha256(preview.revision, 'skill catalog revision'),
currentSnippet: utf8String(
preview.currentSnippet,
'current skill snippet',
PREVIEW_SNIPPET_MAX_BYTES,
true,
),
sourceSnippet: utf8String(
preview.sourceSnippet,
'source skill snippet',
PREVIEW_SNIPPET_MAX_BYTES,
true,
),
currentTruncated: bool(preview.currentTruncated, 'current skill snippet truncated'),
sourceTruncated: bool(preview.sourceTruncated, 'source skill snippet truncated'),
hasManagedBaseline: bool(preview.hasManagedBaseline, 'managed skill baseline availability'),
summary: lineSummary(preview.summary),
expectedCurrentSha256: sha256(preview.expectedCurrentSha256, 'expected current sha256'),
expectedSourceSha256: sha256(preview.expectedSourceSha256, 'expected source sha256'),
};
assertJsonByteLimit(
decoded,
SKILL_CATALOG_PREVIEW_RESULT_MAX_BYTES,
'Skill catalog preview result',
);
return decoded;
}
function localContext(value: unknown): SkillCatalogLocalContext {
const record = requireExactRecord(value, 'skill catalog local context', ['projectRoot']);
const projectRoot = utf8String(
record.projectRoot,
'skill catalog project root',
PROJECT_ROOT_MAX_BYTES,
);
if (!isSkillCatalogProjectRootLexicallyAbsolute(projectRoot)) {
throw invalidProtocolFrame('Skill catalog project root must be absolute');
}
return {
projectRoot,
};
}
function invocableTarget(value: unknown): SkillCatalogInvocableTarget {
const record = requireRecord(value, 'invocable skill catalog target');
if (record.kind === 'session') {
const session = requireExactRecord(record, 'invocable Session target', ['kind', 'sessionId']);
return {
kind: 'session',
sessionId: requireEntityId(session.sessionId, 'invocable Skill Session id'),
};
}
if (record.kind === 'new_session') {
const fresh = requireExactRecord(record, 'invocable new Session target', [
'kind',
'context',
'collaborationMode',
]);
if (fresh.collaborationMode !== 'agent' && fresh.collaborationMode !== 'plan') {
throw invalidProtocolFrame('Invalid invocable Skill collaboration mode');
}
return {
kind: 'new_session',
context: localContext(fresh.context),
collaborationMode: fresh.collaborationMode,
};
}
throw invalidProtocolFrame('Invalid invocable skill catalog target');
}
function invocableItem(value: unknown): SkillCatalogInvocableItem {
const record = requireExactRecord(value, 'invocable skill catalog item', [
'ref',
'id',
'name',
'description',
]);
return {
ref: utf8String(record.ref, 'invocable skill ref', SKILL_CATALOG_REF_MAX_BYTES),
id: utf8String(record.id, 'invocable skill id', SKILL_CATALOG_DISPLAY_ID_MAX_BYTES),
name: utf8String(record.name, 'invocable skill name', SKILL_CATALOG_NAME_MAX_BYTES),
description: utf8String(
record.description,
'invocable skill description',
SKILL_CATALOG_DESCRIPTION_MAX_BYTES,
),
};
}
export function isSkillCatalogProjectRootLexicallyAbsolute(
value: string,
platform: NodeJS.Platform = process.platform,
): boolean {
if (platform !== 'win32') return value.startsWith('/');
return /^[A-Za-z]:[\\/]/.test(value) || /^[\\/]{2}[^\\/]+[\\/][^\\/]+(?:[\\/]|$)/.test(value);
}
function revisionChanged(
value: unknown,
): Extract<SkillCatalogQueryResult, { kind: 'revision_changed' }> {
const record = requireExactRecord(value, 'skill catalog revision changed result', [
'kind',
'expectedRevision',
'actualRevision',
]);
return {
kind: 'revision_changed',
expectedRevision: sha256(record.expectedRevision, 'expected skill catalog revision'),
actualRevision: sha256(record.actualRevision, 'actual skill catalog revision'),
};
}
function revisionConflict(value: unknown): SkillCatalogRevisionConflict {
const record = requireExactRecord(value, 'skill catalog revision conflict', [
'kind',
'expectedRevision',
'actualRevision',
]);
return {
kind: 'revision_conflict',
expectedRevision: sha256(record.expectedRevision, 'expected skill catalog revision'),
actualRevision: sha256(record.actualRevision, 'actual skill catalog revision'),
};
}
function lineSummary(value: unknown): SkillCatalogPreviewLineSummary {
const record = requireExactRecord(value, 'skill catalog preview line summary', [
'currentLineCount',
'sourceLineCount',
'changedLineCount',
]);
return {
currentLineCount: requireCount(record.currentLineCount, 'current skill line count'),
sourceLineCount: requireCount(record.sourceLineCount, 'source skill line count'),
changedLineCount: requireCount(record.changedLineCount, 'changed skill line count'),
};
}
function utf8String(value: unknown, label: string, maxBytes: number, allowEmpty = false): string {
if (
typeof value !== 'string' ||
(!allowEmpty && value.length === 0) ||
Buffer.byteLength(value, 'utf8') > maxBytes
) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value;
}
function id(value: unknown, label: string): string {
const decoded = utf8String(value, label, ID_MAX_BYTES);
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$/.test(decoded)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return decoded;
}
function displayId(value: unknown): string {
return wireIdentity(value, 'skill id', SKILL_CATALOG_DISPLAY_ID_MAX_BYTES);
}
function ref(value: unknown): string {
return wireIdentity(value, 'skill ref', SKILL_CATALOG_REF_MAX_BYTES);
}
function wireIdentity(value: unknown, label: string, maxBytes: number): string {
const decoded = utf8String(value, label, maxBytes);
if (/[\u0000-\u001F\u007F]/.test(decoded)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return decoded;
}
function sha256(value: unknown, label: string): SkillCatalogRevision {
if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as SkillCatalogRevision;
}
function nullableSha256(value: unknown, label: string): SkillContentSha256 | null {
return value === null ? null : sha256(value, label);
}
function stringArray(value: unknown, label: string): readonly string[] {
if (!Array.isArray(value) || value.length > SKILL_CATALOG_STRING_ARRAY_MAX_ITEMS) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value.map((item) => utf8String(item, label, SKILL_CATALOG_STRING_ARRAY_ITEM_MAX_BYTES));
}
function validationCodes(value: unknown): readonly SkillCatalogValidationCode[] {
return stringArray(value, 'skill validation codes').map(validationCode);
}
function bool(value: unknown, label: string): boolean {
if (typeof value !== 'boolean') throw invalidProtocolFrame(`Invalid ${label}`);
return value;
}
function positiveCount(value: unknown, label: string): number {
const decoded = requireCount(value, label);
if (decoded === 0) throw invalidProtocolFrame(`Invalid ${label}`);
return decoded;
}
function catalogView(value: unknown): SkillCatalogView {
if (value === 'governance' || value === 'bundled' || value === 'managed_sources') return value;
throw invalidProtocolFrame('Invalid skill catalog view');
}
function entryKind(value: unknown): SkillCatalogEntryKind {
if (value === 'skill' || value === 'discovery_diagnostic') return value;
throw invalidProtocolFrame('Invalid skill catalog governance item kind');
}
function mutableSourceType(value: unknown): 'bundled' | 'managed' {
if (value === 'bundled' || value === 'managed') return value;
throw invalidProtocolFrame('Invalid mutable skill source type');
}
function sourceType(value: unknown): SkillCatalogSourceType {
if (value === 'workspace' || value === 'bundled' || value === 'managed' || value === 'unknown') {
return value;
}
throw invalidProtocolFrame('Invalid skill source type');
}
function validationStatus(value: unknown): SkillCatalogValidationStatus {
if (
value === 'ok' ||
value === 'missing_lock' ||
value === 'modified' ||
value === 'metadata_error'
) {
return value;
}
throw invalidProtocolFrame('Invalid skill validation status');
}
function validationCode(value: string): SkillCatalogValidationCode {
const codes: readonly SkillCatalogValidationCode[] = [
'missing_lock',
'modified',
'invalid_json',
'id_mismatch',
'unsupported_schema',
'invalid_hash',
'write_failed',
'lock_symlink',
'missing_frontmatter',
'malformed_frontmatter',
'missing_name',
'invalid_name',
'name_too_long',
'missing_description',
'invalid_description',
'description_too_long',
'invalid_allowed_tools',
'invalid_required_tools',
'invalid_required_capabilities',
'invalid_license',
'invalid_compatibility',
'compatibility_too_long',
'invalid_metadata',
'invalid_category',
'unsupported_field',
'body_too_large',
'duplicate_id',
'duplicate_name',
'blocked_path',
'read_failed',
'projection_truncated',
];
if (!codes.includes(value as SkillCatalogValidationCode)) {
throw invalidProtocolFrame('Invalid skill validation code');
}
return value as SkillCatalogValidationCode;
}
function managedUpdateStatus(value: unknown): SkillCatalogManagedUpdateStatus {
if (
value === 'not_managed' ||
value === 'source_missing' ||
value === 'up_to_date' ||
value === 'update_available' ||
value === 'local_modified' ||
value === 'metadata_error'
) {
return value;
}
throw invalidProtocolFrame('Invalid managed skill update status');
}
function runtimeStatus(value: unknown): SkillCatalogRuntimeStatus {
if (value === 'enabled' || value === 'disabled' || value === 'state_error') return value;
throw invalidProtocolFrame('Invalid skill runtime status');
}
function scope(value: unknown): SkillCatalogScope {
if (value === 'project' || value === 'workspace' || value === 'user' || value === 'custom') {
return value;
}
throw invalidProtocolFrame('Invalid skill scope');
}
function discoverySource(value: unknown): SkillCatalogDiscoverySource {
if (value === 'maka' || value === 'agents' || value === 'legacy' || value === 'custom') {
return value;
}
throw invalidProtocolFrame('Invalid skill discovery source');
}
function contextStatus(value: unknown): SkillCatalogContextStatus {
if (
value === 'unknown' ||
value === 'advertised' ||
value === 'disabled' ||
value === 'invalid' ||
value === 'host_incompatible' ||
value === 'shadowed' ||
value === 'budget'
) {
return value;
}
throw invalidProtocolFrame('Invalid skill context status');
}
function mutationRejectedReason(value: unknown): SkillCatalogMutationRejectedReason {
if (
value === 'not_found' ||
value === 'already_exists' ||
value === 'blocked_scope' ||
value === 'not_managed' ||
value === 'source_missing' ||
value === 'source_changed' ||
value === 'source_invalid' ||
value === 'local_modified' ||
value === 'metadata_error' ||
value === 'needs_review' ||
value === 'blocked_path' ||
value === 'state_error'
) {
return value;
}
throw invalidProtocolFrame('Invalid skill catalog mutation rejection reason');
}
function previewRejectedReason(value: unknown): SkillCatalogPreviewRejectedReason {
if (
value === 'not_found' ||
value === 'not_managed' ||
value === 'source_missing' ||
value === 'source_invalid' ||
value === 'metadata_error'
) {
return value;
}
throw invalidProtocolFrame('Invalid skill catalog preview rejection reason');
}
function assertJsonByteLimit(value: unknown, maxBytes: number, label: string): void {
if (Buffer.byteLength(JSON.stringify(value), 'utf8') > maxBytes) {
throw invalidProtocolFrame(`${label} exceeds byte limit`);
}
}