blob: 9f28dec59fedefa77af0a9385f353ad8b08272d1 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RuntimeHostProtocolError } from '../protocol/errors.js';
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import {
decodeClientFrame,
decodeHostFrame,
SKILL_CATALOG_OPERATION_SPECS,
SKILL_CATALOG_PAGE_MAX_BYTES,
SKILL_CATALOG_PAGE_MAX_ITEMS,
SKILL_CATALOG_PREVIEW_RESULT_MAX_BYTES,
WORKSPACE_HOST_PATH_MAX_BYTES,
type SkillCatalogBundledItem,
type SkillCatalogGovernanceItem,
type SkillCatalogMutation,
type SkillCatalogPreviewUpdateResult,
type SkillCatalogRevision,
} from '../protocol/index.js';
import { SkillCatalogRepository } from '../server/skill-catalog-repository.js';
const REVISION = `sha256:${'a'.repeat(64)}` as SkillCatalogRevision;
const NEXT_REVISION = `sha256:${'b'.repeat(64)}` as SkillCatalogRevision;
const CONTEXT = {
workspace: {
kind: 'host_path' as const,
path: process.platform === 'win32' ? 'C:\\workspace\\project' : '/workspace/project',
},
};
const RESOLVED_WORKSPACE = {
target: CONTEXT.workspace,
hostCwd: CONTEXT.workspace.path,
};
type IsAssignable<From, To> = [From] extends [To] ? true : false;
type AssertFalse<Value extends false> = Value;
export type SkillCatalogManagedUpdateMutationTypeContract = [
AssertFalse<
IsAssignable<
{
kind: 'update_managed';
ref: 'workspace:legacy:research-brief';
force: true;
expectedCurrentSha256: null;
expectedSourceSha256: null;
},
SkillCatalogMutation
>
>,
AssertFalse<
IsAssignable<
{
kind: 'update_managed';
ref: 'workspace:legacy:research-brief';
force: false;
expectedCurrentSha256: SkillCatalogRevision;
expectedSourceSha256: SkillCatalogRevision;
},
SkillCatalogMutation
>
>,
];
describe('Runtime Host Skill catalog protocol', () => {
test('only treats explicit Host-path Skill workspaces as Host-path input', () => {
assert.equal(
SKILL_CATALOG_OPERATION_SPECS['skill.catalog.query'].usesHostPaths?.({
kind: 'start',
context: { workspace: { kind: 'project', projectId: 'project-1' } },
view: 'governance',
}),
false,
);
assert.equal(
SKILL_CATALOG_OPERATION_SPECS['skill.catalog.query'].usesHostPaths?.({
kind: 'start',
context: CONTEXT,
view: 'governance',
}),
true,
);
assert.equal(
SKILL_CATALOG_OPERATION_SPECS['skill.catalog.mutate'].usesHostPaths?.({
context: { workspace: { kind: 'project', projectId: 'project-1' } },
expectedRevision: REVISION,
mutation: { kind: 'create_starter' },
}),
false,
);
assert.equal(
SKILL_CATALOG_OPERATION_SPECS['skill.catalog.preview-update'].usesHostPaths?.({
context: CONTEXT,
expectedRevision: REVISION,
ref: 'workspace:legacy:research-brief',
}),
true,
);
});
test('decodes bounded Session and new-Session invocable queries and pages', () => {
for (const input of [
{ kind: 'start', target: { kind: 'session', sessionId: 'session-1' } },
{
kind: 'start',
target: {
kind: 'new_session',
context: CONTEXT,
collaborationMode: 'plan',
permissionMode: 'bypass',
},
},
{
kind: 'continue',
target: { kind: 'session', sessionId: 'session-1' },
revision: REVISION,
cursor: 'next',
},
]) {
assert.deepEqual(
decodeClientFrame({
requestId: 'request-1',
operation: 'skill.catalog.invocable.query',
input,
}),
{
requestId: 'request-1',
operation: 'skill.catalog.invocable.query',
input,
},
);
}
const result = {
kind: 'page',
revision: REVISION,
items: [
{ ref: 'project:maka:review', id: 'review', name: 'Review', description: 'Review code' },
],
nextCursor: null,
};
assert.deepEqual(
decodeHostFrame({
requestId: 'request-1',
operation: 'skill.catalog.invocable.query',
ok: true,
result,
}),
{
requestId: 'request-1',
operation: 'skill.catalog.invocable.query',
ok: true,
result,
},
);
for (const error of [
{ code: 'not_found', message: 'Session does not exist' },
{ code: 'session_archived', message: 'Session is archived' },
] as const) {
const refusal = {
requestId: 'request-1',
operation: 'skill.catalog.invocable.query',
ok: false,
error,
};
assert.deepEqual(decodeHostFrame(refusal), refusal);
}
assertInvalidRequest('skill.catalog.invocable.query', {
kind: 'start',
target: { kind: 'new_session', context: CONTEXT, collaborationMode: 'plan' },
});
assertInvalidRequest('skill.catalog.invocable.query', {
kind: 'start',
target: {
kind: 'new_session',
context: CONTEXT,
collaborationMode: 'plan',
permissionMode: 'unrestricted',
},
});
});
test('decodes start and continuation queries with bounded workspace context', () => {
for (const input of [
{
kind: 'start',
context: CONTEXT,
view: 'governance',
},
{ kind: 'start', context: CONTEXT, view: 'bundled' },
{
kind: 'start',
context: CONTEXT,
view: 'managed_sources',
},
{
kind: 'continue',
context: CONTEXT,
view: 'governance',
revision: REVISION,
cursor: 'opaque:+/cursor==',
},
]) {
const frame = request('skill.catalog.query', input);
assert.deepEqual(decodeClientFrame(frame), frame);
}
assertInvalidRequest('skill.catalog.query', {
kind: 'start',
context: { ...CONTEXT, cwd: '/private' },
view: 'governance',
});
assertInvalidRequest('skill.catalog.query', {
kind: 'start',
context: {
workspace: {
kind: 'host_path',
path: '界'.repeat(Math.floor(WORKSPACE_HOST_PATH_MAX_BYTES / 3) + 1),
},
},
view: 'governance',
});
for (const projectRoot of ['.', 'project', 'workspace/project']) {
assertInvalidRequest('skill.catalog.query', {
kind: 'start',
context: { workspace: { kind: 'host_path', path: projectRoot } },
view: 'governance',
});
}
for (const projectRoot of [
'/workspace/project',
'C:\\workspace\\project',
'\\\\server\\share\\project',
]) {
const input = {
kind: 'start',
context: { workspace: { kind: 'host_path', path: projectRoot } },
view: 'governance',
};
const frame = request('skill.catalog.query', input);
assert.deepEqual(decodeClientFrame(frame), frame);
}
for (const projectRoot of ['\\workspace\\project', 'C:workspace\\project']) {
assertInvalidRequest('skill.catalog.query', {
kind: 'start',
context: { workspace: { kind: 'host_path', path: projectRoot } },
view: 'governance',
});
}
assertInvalidRequest('skill.catalog.query', {
kind: 'continue',
context: CONTEXT,
view: 'governance',
revision: `sha256:${'A'.repeat(64)}`,
cursor: 'cursor',
});
assertInvalidRequest('skill.catalog.query', {
kind: 'start',
context: CONTEXT,
view: 'installed',
});
});
test('decodes metadata-only view-specific pages with fixed nullable governance fields', () => {
const pages = [
page('governance', [
governanceItem(),
governanceItem({
kind: 'discovery_diagnostic',
ref: 'diagnostic:project:maka:0',
id: 'source-0',
name: '',
description: '',
declaredTools: [],
sourceType: 'unknown',
validationStatus: 'metadata_error',
validationCodes: ['blocked_path'],
managedUpdateStatus: null,
enabled: false,
runtimeStatus: 'disabled',
scope: 'project',
source: 'maka',
contextStatus: 'invalid',
contextRank: null,
manageable: false,
}),
]),
page('bundled', [
{
kind: 'bundled',
id: 'deep-research',
name: 'Deep research',
description: 'Research a topic',
category: 'Productivity',
declaredTools: ['Read'],
metadataTruncated: false,
installed: true,
},
]),
page('managed_sources', [
{
kind: 'managed_source',
id: 'research-brief',
name: 'Research brief',
description: 'Prepare a research brief',
category: 'Research',
sourceType: 'local',
metadataTruncated: false,
installed: false,
},
]),
];
for (const result of pages) {
const frame = response('skill.catalog.query', result);
assert.deepEqual(decodeHostFrame(frame), frame);
}
const { managedUpdateStatus: _, ...missingNullable } = governanceItem();
assertInvalidResponse('skill.catalog.query', page('governance', [missingNullable]));
for (const forbidden of [
{ ...governanceItem(), path: '/private/SKILL.md' },
{ ...governanceItem(), body: 'private instructions' },
{ ...governanceItem(), contentSha256: REVISION },
]) {
assertInvalidResponse('skill.catalog.query', page('governance', [forbidden]));
}
assertInvalidResponse(
'skill.catalog.query',
page('bundled', [
{
...(pages[1].items[0] as Record<string, unknown>),
content: 'private instructions',
},
]),
);
assertInvalidResponse(
'skill.catalog.query',
page('bundled', [
{
...(pages[1].items[0] as Record<string, unknown>),
category: '',
},
]),
);
assertInvalidResponse('skill.catalog.query', page('bundled', [pages[2].items[0]]));
});
test('encodes actual bundled catalog metadata through the Host output codec', async () => {
const base = await mkdtemp(join(tmpdir(), 'maka-skill-catalog-protocol-'));
const dataRoot = join(base, 'data');
const projectRoot = join(base, 'project');
const homeDirectory = join(base, 'home');
const managedSourcesRoot = join(base, 'managed-sources');
await Promise.all([
mkdir(dataRoot, { recursive: true }),
mkdir(projectRoot, { recursive: true }),
mkdir(homeDirectory, { recursive: true }),
]);
try {
const repository = new SkillCatalogRepository({
runWithRoot: (operation) => operation(dataRoot),
homeDirectory,
managedSourcesRoot,
});
const result = await repository.query(
{
kind: 'start',
view: 'bundled',
},
{ projectRoot },
);
assert.equal(result.kind, 'page');
if (result.kind !== 'page') return;
const bundledItems = result.items.filter(
(item): item is SkillCatalogBundledItem => item.kind === 'bundled',
);
assert.deepEqual(
bundledItems.map((item) => item.id),
['computer-use'],
);
assert.equal(
bundledItems.every((item) => item.category.length > 0),
true,
);
const frame = response('skill.catalog.query', {
...result,
resolvedWorkspace: {
target: { kind: 'host_path', path: projectRoot },
hostCwd: projectRoot,
},
});
assert.deepEqual(decodeHostFrame(frame), frame);
} finally {
await rm(base, { recursive: true, force: true });
}
});
test('allows bounded human ids in governance while rejecting control-character identities', () => {
const humanIdPage = page('governance', [
governanceItem({ id: 'valid skill', ref: 'project:maka:valid skill' }),
]);
assert.deepEqual(
decodeHostFrame(response('skill.catalog.query', humanIdPage)),
response('skill.catalog.query', humanIdPage),
);
assertInvalidResponse(
'skill.catalog.query',
page('governance', [governanceItem({ id: 'bad\nskill' })]),
);
assertInvalidResponse(
'skill.catalog.query',
page('governance', [governanceItem({ ref: 'project:maka:bad\u0000skill' })]),
);
assertInvalidRequest('skill.catalog.mutate', {
context: CONTEXT,
expectedRevision: REVISION,
mutation: { kind: 'install', sourceType: 'managed', sourceId: 'valid skill' },
});
});
test('enforces page item and decoded UTF-8 byte limits', () => {
assertInvalidResponse(
'skill.catalog.query',
page(
'governance',
Array.from({ length: SKILL_CATALOG_PAGE_MAX_ITEMS + 1 }, () => governanceItem()),
),
);
const oversizedPage = page(
'governance',
Array.from({ length: 13 }, (_, index) =>
governanceItem({
ref: `workspace:legacy:skill-${index}`,
id: `skill-${index}`,
description: 'x'.repeat(4096),
}),
),
);
assert.ok(
Buffer.byteLength(JSON.stringify(oversizedPage), 'utf8') > SKILL_CATALOG_PAGE_MAX_BYTES,
);
assertInvalidResponse('skill.catalog.query', oversizedPage);
assertInvalidResponse(
'skill.catalog.query',
page('governance', [governanceItem({ description: '界'.repeat(1366) })]),
);
});
test('decodes all mutation variants and requires preview hashes only for force update', () => {
const maximumLengthId = `s${'a'.repeat(80)}`;
const mutations: readonly SkillCatalogMutation[] = [
{ kind: 'create_starter' },
{ kind: 'install', sourceType: 'bundled', sourceId: 'deep-research' },
{ kind: 'install', sourceType: 'managed', sourceId: 'research-brief' },
{ kind: 'install', sourceType: 'managed', sourceId: maximumLengthId },
{
kind: 'update_managed',
ref: 'workspace:legacy:research-brief',
force: false,
expectedCurrentSha256: null,
expectedSourceSha256: null,
},
{
kind: 'update_managed',
ref: 'workspace:legacy:research-brief',
force: true,
expectedCurrentSha256: REVISION,
expectedSourceSha256: NEXT_REVISION,
},
{ kind: 'delete', ref: 'workspace:legacy:research-brief' },
{ kind: 'set_enabled', ref: 'user:maka:research-brief', enabled: false },
{ kind: 'set_pinned', ref: 'user:maka:research-brief', pinned: true },
];
for (const mutation of mutations) {
const frame = request('skill.catalog.mutate', {
context: CONTEXT,
expectedRevision: REVISION,
mutation,
});
assert.deepEqual(decodeClientFrame(frame), frame);
}
assertInvalidRequest('skill.catalog.mutate', {
context: CONTEXT,
expectedRevision: REVISION,
mutation: {
kind: 'install',
sourceType: 'managed',
sourceId: `${maximumLengthId}a`,
},
});
for (const mutation of [
{
kind: 'update_managed',
ref: 'workspace:legacy:research-brief',
force: true,
expectedCurrentSha256: null,
expectedSourceSha256: NEXT_REVISION,
},
{
kind: 'update_managed',
ref: 'workspace:legacy:research-brief',
force: false,
expectedCurrentSha256: REVISION,
expectedSourceSha256: NEXT_REVISION,
},
{ kind: 'set_pinned', ref: 'user:maka:research-brief', pinned: true, retry: true },
]) {
assertInvalidRequest('skill.catalog.mutate', {
context: CONTEXT,
expectedRevision: REVISION,
mutation,
});
}
});
test('decodes mutation outcomes, revision conflicts, and typed rejections', () => {
for (const result of [
{
kind: 'committed',
revision: NEXT_REVISION,
entry: governanceItem(),
resolvedWorkspace: RESOLVED_WORKSPACE,
},
{
kind: 'unchanged',
revision: REVISION,
entry: null,
resolvedWorkspace: RESOLVED_WORKSPACE,
},
{
kind: 'revision_conflict',
expectedRevision: REVISION,
actualRevision: NEXT_REVISION,
resolvedWorkspace: RESOLVED_WORKSPACE,
},
{ kind: 'rejected', reason: 'blocked_scope', resolvedWorkspace: RESOLVED_WORKSPACE },
{ kind: 'rejected', reason: 'metadata_error', resolvedWorkspace: RESOLVED_WORKSPACE },
]) {
const frame = response('skill.catalog.mutate', result);
assert.deepEqual(decodeHostFrame(frame), frame);
}
assertInvalidResponse('skill.catalog.mutate', {
kind: 'rejected',
reason: 'write_failed',
});
assertInvalidResponse('skill.catalog.mutate', {
kind: 'committed',
revision: NEXT_REVISION,
});
});
test('decodes bounded update previews without baseline content', () => {
const preview: SkillCatalogPreviewUpdateResult = {
kind: 'preview',
revision: REVISION,
currentSnippet: 'old\ncontent\n',
sourceSnippet: 'new\ncontent\n',
currentTruncated: false,
sourceTruncated: false,
hasManagedBaseline: true,
summary: {
currentLineCount: 2,
sourceLineCount: 2,
changedLineCount: 1,
},
expectedCurrentSha256: REVISION,
expectedSourceSha256: NEXT_REVISION,
resolvedWorkspace: RESOLVED_WORKSPACE,
};
const requestFrame = request('skill.catalog.preview-update', {
context: CONTEXT,
expectedRevision: REVISION,
ref: 'workspace:legacy:research-brief',
});
assert.deepEqual(decodeClientFrame(requestFrame), requestFrame);
const responseFrame = response('skill.catalog.preview-update', preview);
assert.deepEqual(decodeHostFrame(responseFrame), responseFrame);
const metadataErrorFrame = response('skill.catalog.preview-update', {
kind: 'rejected',
reason: 'metadata_error',
resolvedWorkspace: RESOLVED_WORKSPACE,
});
assert.deepEqual(decodeHostFrame(metadataErrorFrame), metadataErrorFrame);
assertInvalidResponse('skill.catalog.preview-update', {
...preview,
baselineContent: 'must not cross the protocol boundary',
});
assertInvalidResponse('skill.catalog.preview-update', {
...preview,
expectedCurrentSha256: 'a'.repeat(64),
});
assertInvalidResponse('skill.catalog.preview-update', {
kind: 'rejected',
reason: 'preview_too_large',
});
const oversized = {
...preview,
currentSnippet: 'x'.repeat(24 * 1024),
sourceSnippet: 'y'.repeat(24 * 1024),
currentTruncated: true,
sourceTruncated: true,
};
assert.ok(
Buffer.byteLength(JSON.stringify(oversized), 'utf8') > SKILL_CATALOG_PREVIEW_RESULT_MAX_BYTES,
);
assertInvalidResponse('skill.catalog.preview-update', oversized);
});
test('rejects unknown fields on every result branch', () => {
for (const [operation, result] of [
[
'skill.catalog.query',
{
kind: 'revision_changed',
expectedRevision: REVISION,
actualRevision: NEXT_REVISION,
retry: true,
},
],
[
'skill.catalog.mutate',
{
kind: 'revision_conflict',
expectedRevision: REVISION,
actualRevision: NEXT_REVISION,
retry: true,
},
],
[
'skill.catalog.preview-update',
{ kind: 'rejected', reason: 'not_found', detail: '/private/SKILL.md' },
],
] as const) {
assertInvalidResponse(operation, result);
}
});
});
function governanceItem(
overrides: Partial<SkillCatalogGovernanceItem> = {},
): SkillCatalogGovernanceItem {
return {
kind: 'skill',
ref: 'workspace:legacy:research-brief',
id: 'research-brief',
name: 'Research brief',
description: 'Prepare a research brief',
declaredTools: ['Read'],
metadataTruncated: false,
sourceType: 'managed',
userModified: false,
validationStatus: 'ok',
validationCodes: [],
managedUpdateStatus: 'up_to_date',
enabled: true,
pinned: false,
runtimeStatus: 'enabled',
scope: 'workspace',
source: 'legacy',
contextStatus: 'unknown',
contextRank: null,
shadowedBy: null,
needsReview: false,
manageable: true,
...overrides,
};
}
function page(view: 'governance' | 'bundled' | 'managed_sources', items: readonly unknown[]) {
return {
kind: 'page',
view,
revision: REVISION,
items,
nextCursor: null,
resolvedWorkspace: RESOLVED_WORKSPACE,
};
}
function request(operation: string, input: unknown) {
return { requestId: 'request-1', operation, input };
}
function response(operation: string, result: unknown) {
return { requestId: 'request-1', operation, ok: true, result };
}
function assertInvalidRequest(operation: string, input: unknown): void {
assert.throws(() => decodeClientFrame(request(operation, input)), isInvalidFrame);
}
function assertInvalidResponse(operation: string, result: unknown): void {
assert.throws(() => decodeHostFrame(response(operation, result)), isInvalidFrame);
}
function isInvalidFrame(error: unknown): boolean {
return error instanceof RuntimeHostProtocolError && error.code === 'invalid_frame';
}