feat: share Skill REST DTO authority
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// Generated from workspace-api. Do not edit by hand.
|
||||
// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_skill_api_types > web/workspace/src/lib/generated/skill-api.ts
|
||||
|
||||
export const SKILL_API_AUTHORITY = "workspace-config-skills-v1" as const;
|
||||
|
||||
export const SKILL_API_LIMITS = {
|
||||
maxSafeInteger: 9007199254740991,
|
||||
maxCatalogEntries: 500,
|
||||
maxOverrides: 64,
|
||||
maxDiagnostics: 100,
|
||||
maxResources: 500,
|
||||
maxAllowedTools: 100,
|
||||
maxNameBytes: 128,
|
||||
maxLabelBytes: 4096,
|
||||
maxBodyBytes: 1048576,
|
||||
maxPathBytes: 1024,
|
||||
maxDigestBytes: 128,
|
||||
} as const;
|
||||
|
||||
export type SkillDiagnosticSeverity = "error" | "warning";
|
||||
|
||||
export type SkillDiagnostic = {
|
||||
severity: SkillDiagnosticSeverity;
|
||||
code: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type SkillSourceKind = "builtin" | "workspace";
|
||||
|
||||
export type SkillProvenance = {
|
||||
kind: SkillSourceKind;
|
||||
id: string;
|
||||
virtual_path?: string;
|
||||
revision?: number;
|
||||
source_digest?: string;
|
||||
tree_digest?: string;
|
||||
};
|
||||
|
||||
export type SkillActivationStatus = "active" | "inactive";
|
||||
|
||||
export type SkillProjectionStatus = "valid" | "invalid";
|
||||
|
||||
export type SkillProjectionIdentity = {
|
||||
config_revision: number;
|
||||
tree_digest: string;
|
||||
};
|
||||
|
||||
export type SkillResourceRef = {
|
||||
kind: string;
|
||||
name: string;
|
||||
supported: boolean;
|
||||
diagnostic?: string;
|
||||
};
|
||||
|
||||
export type SkillCatalogEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
activation_status: SkillActivationStatus;
|
||||
projection_status: SkillProjectionStatus;
|
||||
provenance: SkillProvenance;
|
||||
overrides: Array<SkillProvenance>;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
};
|
||||
|
||||
export type SkillCatalogResponse = {
|
||||
authority: string;
|
||||
projection: SkillProjectionIdentity;
|
||||
entries: Array<SkillCatalogEntry>;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
};
|
||||
|
||||
export type SkillDetailResponse = {
|
||||
authority: string;
|
||||
projection: SkillProjectionIdentity;
|
||||
name: string;
|
||||
description: string;
|
||||
provenance: SkillProvenance;
|
||||
overrides: Array<SkillProvenance>;
|
||||
diagnostics: Array<SkillDiagnostic>;
|
||||
activation_status: SkillActivationStatus;
|
||||
projection_status: SkillProjectionStatus;
|
||||
body: string;
|
||||
allowed_tools: Array<string>;
|
||||
allowed_tools_status: string;
|
||||
resources: Array<SkillResourceRef>;
|
||||
};
|
||||
@@ -89,11 +89,24 @@ Deno.test("loadWorkspaceSkillCatalog fetches lightweight catalog", async () => {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
authority: "workspace-backend-skills-v0",
|
||||
authority: "workspace-config-skills-v1",
|
||||
projection: {
|
||||
config_revision: 7,
|
||||
tree_digest: "tree-digest",
|
||||
},
|
||||
entries: [{
|
||||
name: "triage-errors",
|
||||
description: "Use when triaging errors.",
|
||||
provenance: { kind: "workspace", id: "workspace:triage-errors" },
|
||||
activation_status: "active",
|
||||
projection_status: "valid",
|
||||
provenance: {
|
||||
kind: "workspace",
|
||||
id: "workspace:triage-errors",
|
||||
virtual_path: "skills/triage-errors/SKILL.md",
|
||||
revision: 7,
|
||||
source_digest: "source-digest",
|
||||
tree_digest: "tree-digest",
|
||||
},
|
||||
overrides: [],
|
||||
diagnostics: [],
|
||||
}],
|
||||
|
||||
@@ -1,58 +1,18 @@
|
||||
import type {
|
||||
SkillCatalogResponse,
|
||||
SkillDetailResponse,
|
||||
} from "$lib/generated/skill-api.ts";
|
||||
import {
|
||||
parseSkillCatalogResponse,
|
||||
parseSkillDetailResponse,
|
||||
} from "$lib/workspace/skills/api.ts";
|
||||
|
||||
export type ApiResult<T> = {
|
||||
data: T | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export type SkillDiagnosticSeverity = "error" | "warning";
|
||||
|
||||
export type SkillDiagnostic = {
|
||||
severity: SkillDiagnosticSeverity;
|
||||
code: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type SkillProvenance = {
|
||||
kind: "builtin" | "workspace";
|
||||
id: string;
|
||||
virtual_path?: string;
|
||||
revision?: number;
|
||||
source_digest?: string;
|
||||
tree_digest?: string;
|
||||
};
|
||||
|
||||
export type SkillCatalogEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
provenance: SkillProvenance;
|
||||
overrides: SkillProvenance[];
|
||||
diagnostics: SkillDiagnostic[];
|
||||
};
|
||||
|
||||
export type SkillCatalogResponse = {
|
||||
authority: string;
|
||||
entries: SkillCatalogEntry[];
|
||||
diagnostics: SkillDiagnostic[];
|
||||
};
|
||||
|
||||
export type SkillResourceRef = {
|
||||
kind: string;
|
||||
name: string;
|
||||
supported: boolean;
|
||||
diagnostic?: string;
|
||||
};
|
||||
|
||||
export type SkillDetailResponse = {
|
||||
name: string;
|
||||
description: string;
|
||||
provenance: SkillProvenance;
|
||||
overrides: SkillProvenance[];
|
||||
diagnostics: SkillDiagnostic[];
|
||||
body: string;
|
||||
allowed_tools: string[];
|
||||
allowed_tools_status: string;
|
||||
resources: SkillResourceRef[];
|
||||
};
|
||||
export type { SkillCatalogResponse, SkillDetailResponse };
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
if (!path || path === "/") return "";
|
||||
@@ -95,6 +55,8 @@ export async function loadWorkspaceSkillCatalog(
|
||||
return loadJson<SkillCatalogResponse>(
|
||||
fetchFn,
|
||||
workspaceSkillCatalogPath(workspaceId),
|
||||
undefined,
|
||||
parseSkillCatalogResponse,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +68,8 @@ export async function loadWorkspaceSkillDetail(
|
||||
return loadJson<SkillDetailResponse>(
|
||||
fetchFn,
|
||||
workspaceSkillDetailPath(workspaceId, name),
|
||||
undefined,
|
||||
parseSkillDetailResponse,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
import {
|
||||
SKILL_API_AUTHORITY,
|
||||
SKILL_API_LIMITS,
|
||||
type SkillActivationStatus,
|
||||
type SkillCatalogEntry,
|
||||
type SkillCatalogResponse,
|
||||
type SkillDetailResponse,
|
||||
type SkillDiagnostic,
|
||||
type SkillDiagnosticSeverity,
|
||||
type SkillProjectionIdentity,
|
||||
type SkillProjectionStatus,
|
||||
type SkillProvenance,
|
||||
type SkillResourceRef,
|
||||
type SkillSourceKind,
|
||||
} from "$lib/generated/skill-api.ts";
|
||||
|
||||
export class SkillApiContractError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "SkillApiContractError";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSkillCatalogResponse(
|
||||
value: unknown,
|
||||
): SkillCatalogResponse {
|
||||
const record = strictObject(value, [
|
||||
"authority",
|
||||
"projection",
|
||||
"entries",
|
||||
"diagnostics",
|
||||
], "Skill catalog response");
|
||||
const authority = boundedString(
|
||||
record.authority,
|
||||
"Skill catalog authority",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
);
|
||||
if (authority !== SKILL_API_AUTHORITY) {
|
||||
throw contractError("unsupported Skill catalog authority");
|
||||
}
|
||||
const projection = parseProjection(record.projection);
|
||||
return {
|
||||
authority,
|
||||
projection,
|
||||
entries: boundedArray(
|
||||
record.entries,
|
||||
"Skill catalog entries",
|
||||
SKILL_API_LIMITS.maxCatalogEntries,
|
||||
).map((entry) => parseCatalogEntry(entry, projection)),
|
||||
diagnostics: parseDiagnostics(record.diagnostics),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSkillDetailResponse(value: unknown): SkillDetailResponse {
|
||||
const record = strictObject(value, [
|
||||
"authority",
|
||||
"projection",
|
||||
"name",
|
||||
"description",
|
||||
"provenance",
|
||||
"overrides",
|
||||
"diagnostics",
|
||||
"activation_status",
|
||||
"projection_status",
|
||||
"body",
|
||||
"allowed_tools",
|
||||
"allowed_tools_status",
|
||||
"resources",
|
||||
], "Skill detail response");
|
||||
const authority = boundedString(
|
||||
record.authority,
|
||||
"Skill detail authority",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
);
|
||||
if (authority !== SKILL_API_AUTHORITY) {
|
||||
throw contractError("unsupported Skill detail authority");
|
||||
}
|
||||
const projection = parseProjection(record.projection);
|
||||
return {
|
||||
authority,
|
||||
projection,
|
||||
name: boundedString(
|
||||
record.name,
|
||||
"Skill name",
|
||||
SKILL_API_LIMITS.maxNameBytes,
|
||||
false,
|
||||
),
|
||||
description: boundedString(
|
||||
record.description,
|
||||
"Skill description",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
true,
|
||||
),
|
||||
provenance: parseProvenance(record.provenance, projection),
|
||||
overrides: parseProvenances(record.overrides, projection),
|
||||
diagnostics: parseDiagnostics(record.diagnostics),
|
||||
activation_status: activationStatus(record.activation_status),
|
||||
projection_status: projectionStatus(record.projection_status),
|
||||
body: boundedString(
|
||||
record.body,
|
||||
"Skill body",
|
||||
SKILL_API_LIMITS.maxBodyBytes,
|
||||
true,
|
||||
),
|
||||
allowed_tools: boundedArray(
|
||||
record.allowed_tools,
|
||||
"Skill allowed tools",
|
||||
SKILL_API_LIMITS.maxAllowedTools,
|
||||
).map((tool) =>
|
||||
boundedString(
|
||||
tool,
|
||||
"Skill allowed tool",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
)
|
||||
),
|
||||
allowed_tools_status: boundedString(
|
||||
record.allowed_tools_status,
|
||||
"Skill allowed-tools status",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
),
|
||||
resources: boundedArray(
|
||||
record.resources,
|
||||
"Skill resources",
|
||||
SKILL_API_LIMITS.maxResources,
|
||||
).map(parseResource),
|
||||
};
|
||||
}
|
||||
|
||||
function parseCatalogEntry(
|
||||
value: unknown,
|
||||
projection: SkillProjectionIdentity,
|
||||
): SkillCatalogEntry {
|
||||
const record = strictObject(value, [
|
||||
"name",
|
||||
"description",
|
||||
"activation_status",
|
||||
"projection_status",
|
||||
"provenance",
|
||||
"overrides",
|
||||
"diagnostics",
|
||||
], "Skill catalog entry");
|
||||
return {
|
||||
name: boundedString(
|
||||
record.name,
|
||||
"Skill name",
|
||||
SKILL_API_LIMITS.maxNameBytes,
|
||||
false,
|
||||
),
|
||||
description: boundedString(
|
||||
record.description,
|
||||
"Skill description",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
true,
|
||||
),
|
||||
activation_status: activationStatus(record.activation_status),
|
||||
projection_status: projectionStatus(record.projection_status),
|
||||
provenance: parseProvenance(record.provenance, projection),
|
||||
overrides: parseProvenances(record.overrides, projection),
|
||||
diagnostics: parseDiagnostics(record.diagnostics),
|
||||
};
|
||||
}
|
||||
|
||||
function parseProjection(value: unknown): SkillProjectionIdentity {
|
||||
const record = strictObject(
|
||||
value,
|
||||
["config_revision", "tree_digest"],
|
||||
"Skill projection identity",
|
||||
);
|
||||
return {
|
||||
config_revision: safeInteger(
|
||||
record.config_revision,
|
||||
"Skill config revision",
|
||||
),
|
||||
tree_digest: boundedString(
|
||||
record.tree_digest,
|
||||
"Skill tree digest",
|
||||
SKILL_API_LIMITS.maxDigestBytes,
|
||||
false,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseProvenances(
|
||||
value: unknown,
|
||||
projection: SkillProjectionIdentity,
|
||||
): SkillProvenance[] {
|
||||
return boundedArray(
|
||||
value,
|
||||
"Skill overrides",
|
||||
SKILL_API_LIMITS.maxOverrides,
|
||||
).map((provenance) => parseProvenance(provenance, projection));
|
||||
}
|
||||
|
||||
function parseProvenance(
|
||||
value: unknown,
|
||||
projection: SkillProjectionIdentity,
|
||||
): SkillProvenance {
|
||||
const record = strictObject(
|
||||
value,
|
||||
[
|
||||
"kind",
|
||||
"id",
|
||||
"virtual_path",
|
||||
"revision",
|
||||
"source_digest",
|
||||
"tree_digest",
|
||||
],
|
||||
"Skill provenance",
|
||||
[
|
||||
"virtual_path",
|
||||
"revision",
|
||||
"source_digest",
|
||||
"tree_digest",
|
||||
],
|
||||
);
|
||||
const kind = sourceKind(record.kind);
|
||||
const id = boundedString(
|
||||
record.id,
|
||||
"Skill provenance id",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
);
|
||||
const virtualPath = optionalBoundedString(
|
||||
record.virtual_path,
|
||||
"Skill virtual path",
|
||||
SKILL_API_LIMITS.maxPathBytes,
|
||||
);
|
||||
const sourceDigest = optionalBoundedString(
|
||||
record.source_digest,
|
||||
"Skill source digest",
|
||||
SKILL_API_LIMITS.maxDigestBytes,
|
||||
);
|
||||
const treeDigest = optionalBoundedString(
|
||||
record.tree_digest,
|
||||
"Skill provenance tree digest",
|
||||
SKILL_API_LIMITS.maxDigestBytes,
|
||||
);
|
||||
const revision = record.revision === undefined
|
||||
? undefined
|
||||
: safeInteger(record.revision, "Skill provenance revision");
|
||||
|
||||
if (
|
||||
!id.startsWith(`${kind}:`) || virtualPath === undefined ||
|
||||
sourceDigest === undefined || !isVirtualPath(virtualPath)
|
||||
) {
|
||||
throw contractError("invalid Skill provenance");
|
||||
}
|
||||
if (kind === "builtin") {
|
||||
if (revision !== undefined || treeDigest !== undefined) {
|
||||
throw contractError("invalid built-in Skill provenance");
|
||||
}
|
||||
} else {
|
||||
if (revision === undefined || treeDigest === undefined) {
|
||||
throw contractError("incomplete Workspace Skill provenance");
|
||||
}
|
||||
if (
|
||||
revision !== projection.config_revision ||
|
||||
treeDigest !== projection.tree_digest
|
||||
) {
|
||||
throw contractError("stale Workspace Skill projection");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind,
|
||||
id,
|
||||
virtual_path: virtualPath,
|
||||
revision,
|
||||
source_digest: sourceDigest,
|
||||
tree_digest: treeDigest,
|
||||
};
|
||||
}
|
||||
|
||||
function parseDiagnostics(value: unknown): SkillDiagnostic[] {
|
||||
return boundedArray(
|
||||
value,
|
||||
"Skill diagnostics",
|
||||
SKILL_API_LIMITS.maxDiagnostics,
|
||||
).map((diagnostic) => {
|
||||
const record = strictObject(
|
||||
diagnostic,
|
||||
[
|
||||
"severity",
|
||||
"code",
|
||||
"message",
|
||||
"source",
|
||||
],
|
||||
"Skill diagnostic",
|
||||
["source"],
|
||||
);
|
||||
return {
|
||||
severity: diagnosticSeverity(record.severity),
|
||||
code: boundedString(
|
||||
record.code,
|
||||
"Skill diagnostic code",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
),
|
||||
message: boundedString(
|
||||
record.message,
|
||||
"Skill diagnostic message",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
),
|
||||
source: optionalBoundedString(
|
||||
record.source,
|
||||
"Skill diagnostic source",
|
||||
SKILL_API_LIMITS.maxPathBytes,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseResource(value: unknown): SkillResourceRef {
|
||||
const record = strictObject(
|
||||
value,
|
||||
[
|
||||
"kind",
|
||||
"name",
|
||||
"supported",
|
||||
"diagnostic",
|
||||
],
|
||||
"Skill resource",
|
||||
["diagnostic"],
|
||||
);
|
||||
if (typeof record.supported !== "boolean") {
|
||||
throw contractError("Skill resource supported must be a boolean");
|
||||
}
|
||||
const name = boundedString(
|
||||
record.name,
|
||||
"Skill resource name",
|
||||
SKILL_API_LIMITS.maxPathBytes,
|
||||
false,
|
||||
);
|
||||
if (!isVirtualPath(name)) {
|
||||
throw contractError("invalid Skill resource virtual path");
|
||||
}
|
||||
return {
|
||||
kind: boundedString(
|
||||
record.kind,
|
||||
"Skill resource kind",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
false,
|
||||
),
|
||||
name,
|
||||
supported: record.supported,
|
||||
diagnostic: optionalBoundedString(
|
||||
record.diagnostic,
|
||||
"Skill resource diagnostic",
|
||||
SKILL_API_LIMITS.maxLabelBytes,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function sourceKind(value: unknown): SkillSourceKind {
|
||||
if (value === "builtin" || value === "workspace") return value;
|
||||
throw contractError("unsupported Skill provenance kind");
|
||||
}
|
||||
|
||||
function diagnosticSeverity(value: unknown): SkillDiagnosticSeverity {
|
||||
if (value === "error" || value === "warning") return value;
|
||||
throw contractError("unsupported Skill diagnostic severity");
|
||||
}
|
||||
|
||||
function activationStatus(value: unknown): SkillActivationStatus {
|
||||
if (value === "active" || value === "inactive") return value;
|
||||
throw contractError("unsupported Skill activation status");
|
||||
}
|
||||
|
||||
function projectionStatus(value: unknown): SkillProjectionStatus {
|
||||
if (value === "valid" || value === "invalid") return value;
|
||||
throw contractError("unsupported Skill projection status");
|
||||
}
|
||||
|
||||
function safeInteger(value: unknown, label: string): number {
|
||||
if (
|
||||
typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 ||
|
||||
value > SKILL_API_LIMITS.maxSafeInteger
|
||||
) {
|
||||
throw contractError(`${label} must be a non-negative safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function boundedArray(
|
||||
value: unknown,
|
||||
label: string,
|
||||
limit: number,
|
||||
): unknown[] {
|
||||
if (!Array.isArray(value) || value.length > limit) {
|
||||
throw contractError(`${label} must be a bounded array`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalBoundedString(
|
||||
value: unknown,
|
||||
label: string,
|
||||
limit: number,
|
||||
): string | undefined {
|
||||
return value === undefined
|
||||
? undefined
|
||||
: boundedString(value, label, limit, false);
|
||||
}
|
||||
|
||||
function boundedString(
|
||||
value: unknown,
|
||||
label: string,
|
||||
limit: number,
|
||||
allowEmpty: boolean,
|
||||
): string {
|
||||
if (
|
||||
typeof value !== "string" || (!allowEmpty && value.length === 0) ||
|
||||
new TextEncoder().encode(value).length > limit
|
||||
) {
|
||||
throw contractError(`${label} must be a bounded string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isVirtualPath(value: string): boolean {
|
||||
return !value.startsWith("/") && !value.includes("\\") &&
|
||||
value.split("/").every((part) =>
|
||||
part !== "" && part !== "." && part !== ".."
|
||||
);
|
||||
}
|
||||
|
||||
function strictObject(
|
||||
value: unknown,
|
||||
allowedKeys: readonly string[],
|
||||
label: string,
|
||||
optionalKeys: readonly string[] = [],
|
||||
): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw contractError(`${label} must be an object`);
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const allowed = new Set(allowedKeys);
|
||||
if (Object.keys(record).some((key) => !allowed.has(key))) {
|
||||
throw contractError(`${label} contains unknown fields`);
|
||||
}
|
||||
const optional = new Set(optionalKeys);
|
||||
if (allowedKeys.some((key) => !optional.has(key) && !(key in record))) {
|
||||
throw contractError(`${label} is missing required fields`);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function contractError(message: string): SkillApiContractError {
|
||||
return new SkillApiContractError(message.slice(0, 256));
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
parseSkillCatalogResponse,
|
||||
parseSkillDetailResponse,
|
||||
SkillApiContractError,
|
||||
} from "../src/lib/workspace/skills/api.ts";
|
||||
import { SKILL_API_LIMITS } from "../src/lib/generated/skill-api.ts";
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => Promise<void> | void): void;
|
||||
};
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function assertEquals<T>(actual: T, expected: T): void {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(
|
||||
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertContractError(value: () => unknown, expected: string): void {
|
||||
try {
|
||||
value();
|
||||
} catch (error) {
|
||||
assert(
|
||||
error instanceof SkillApiContractError,
|
||||
"expected SkillApiContractError",
|
||||
);
|
||||
assert(
|
||||
error.message.includes(expected),
|
||||
`expected bounded diagnostic containing ${expected}, got ${error.message}`,
|
||||
);
|
||||
assert(error.message.length <= 256, "diagnostic must remain bounded");
|
||||
return;
|
||||
}
|
||||
throw new Error("expected parser to reject malformed Skill response");
|
||||
}
|
||||
|
||||
function builtinProvenance() {
|
||||
return {
|
||||
kind: "builtin",
|
||||
id: "builtin:errors",
|
||||
virtual_path: "skills/errors/SKILL.md",
|
||||
source_digest: "builtin-source-digest",
|
||||
};
|
||||
}
|
||||
|
||||
function workspaceProvenance() {
|
||||
return {
|
||||
kind: "workspace",
|
||||
id: "workspace:release",
|
||||
virtual_path: "skills/release/SKILL.md",
|
||||
revision: 42,
|
||||
source_digest: "workspace-source-digest",
|
||||
tree_digest: "tree-digest",
|
||||
};
|
||||
}
|
||||
|
||||
function catalogFixture(): Record<string, unknown> {
|
||||
return {
|
||||
authority: "workspace-config-skills-v1",
|
||||
projection: { config_revision: 42, tree_digest: "tree-digest" },
|
||||
entries: [{
|
||||
name: "errors",
|
||||
description: "Builtin guidance",
|
||||
activation_status: "active",
|
||||
projection_status: "valid",
|
||||
provenance: builtinProvenance(),
|
||||
overrides: [],
|
||||
diagnostics: [],
|
||||
}, {
|
||||
name: "release",
|
||||
description: "Workspace guidance",
|
||||
activation_status: "inactive",
|
||||
projection_status: "invalid",
|
||||
provenance: workspaceProvenance(),
|
||||
overrides: [builtinProvenance()],
|
||||
diagnostics: [{
|
||||
severity: "error",
|
||||
code: "invalid_projection",
|
||||
message: "invalid projected Skill",
|
||||
source: "workspace:release",
|
||||
}],
|
||||
}],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
function detailFixture(): Record<string, unknown> {
|
||||
return {
|
||||
authority: "workspace-config-skills-v1",
|
||||
projection: { config_revision: 42, tree_digest: "tree-digest" },
|
||||
name: "release",
|
||||
description: "Workspace guidance",
|
||||
provenance: workspaceProvenance(),
|
||||
overrides: [],
|
||||
diagnostics: [],
|
||||
activation_status: "active",
|
||||
projection_status: "valid",
|
||||
body: "# Release\n",
|
||||
allowed_tools: ["Bash"],
|
||||
allowed_tools_status: "experimental_hint_only",
|
||||
resources: [{
|
||||
kind: "reference",
|
||||
name: "skills/release/references/checklist.md",
|
||||
supported: true,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("Skill catalog parser accepts generated builtin, Workspace, and invalid projection shapes", () => {
|
||||
const parsed = parseSkillCatalogResponse(catalogFixture());
|
||||
assertEquals(parsed.entries.length, 2);
|
||||
assertEquals(parsed.entries[0].provenance.kind, "builtin");
|
||||
assertEquals(parsed.entries[1].activation_status, "inactive");
|
||||
assertEquals(parsed.entries[1].projection_status, "invalid");
|
||||
assertEquals(parsed.projection.config_revision, 42);
|
||||
});
|
||||
|
||||
Deno.test("Skill detail parser preserves shared generated DTO fields", () => {
|
||||
const parsed = parseSkillDetailResponse(detailFixture());
|
||||
assertEquals(parsed.name, "release");
|
||||
assertEquals(parsed.allowed_tools, ["Bash"]);
|
||||
assertEquals(parsed.resources[0].supported, true);
|
||||
});
|
||||
|
||||
Deno.test("Skill parser rejects stale Workspace projection revision and digest", () => {
|
||||
const staleRevision = catalogFixture();
|
||||
(staleRevision.projection as Record<string, unknown>).config_revision = 43;
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(staleRevision),
|
||||
"stale Workspace Skill projection",
|
||||
);
|
||||
|
||||
const staleDigest = catalogFixture();
|
||||
(staleDigest.projection as Record<string, unknown>).tree_digest = "new-tree";
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(staleDigest),
|
||||
"stale Workspace Skill projection",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Skill parser fails closed on unknown fields and newer enum values", () => {
|
||||
const unknownField = catalogFixture();
|
||||
unknownField.unexpected = true;
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(unknownField),
|
||||
"unknown fields",
|
||||
);
|
||||
|
||||
const newerProvenance = catalogFixture();
|
||||
const entries = newerProvenance.entries as Record<string, unknown>[];
|
||||
(entries[0].provenance as Record<string, unknown>).kind = "remote_catalog";
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(newerProvenance),
|
||||
"unsupported Skill provenance kind",
|
||||
);
|
||||
|
||||
const newerStatus = catalogFixture();
|
||||
const newerEntries = newerStatus.entries as Record<string, unknown>[];
|
||||
newerEntries[0].projection_status = "stale";
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(newerStatus),
|
||||
"unsupported Skill projection status",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Skill parser rejects unsafe revisions and oversized collections or strings", () => {
|
||||
const unsafeRevision = catalogFixture();
|
||||
(unsafeRevision.projection as Record<string, unknown>).config_revision =
|
||||
Number.MAX_SAFE_INTEGER + 1;
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(unsafeRevision),
|
||||
"safe integer",
|
||||
);
|
||||
|
||||
const oversizedCatalog = catalogFixture();
|
||||
const firstEntry = (oversizedCatalog.entries as unknown[])[0];
|
||||
oversizedCatalog.entries = Array.from(
|
||||
{ length: SKILL_API_LIMITS.maxCatalogEntries + 1 },
|
||||
() => firstEntry,
|
||||
);
|
||||
assertContractError(
|
||||
() => parseSkillCatalogResponse(oversizedCatalog),
|
||||
"bounded array",
|
||||
);
|
||||
|
||||
const oversizedDetail = detailFixture();
|
||||
oversizedDetail.body = "x".repeat(SKILL_API_LIMITS.maxBodyBytes + 1);
|
||||
assertContractError(
|
||||
() => parseSkillDetailResponse(oversizedDetail),
|
||||
"bounded string",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Skill parser diagnostics never include rejected Skill body content", () => {
|
||||
const secret = "SENSITIVE-SKILL-BODY-CONTENT";
|
||||
const malformed = detailFixture();
|
||||
malformed.body = secret;
|
||||
malformed.provenance = {
|
||||
...workspaceProvenance(),
|
||||
kind: "newer_source_kind",
|
||||
};
|
||||
try {
|
||||
parseSkillDetailResponse(malformed);
|
||||
throw new Error("expected malformed provenance to fail");
|
||||
} catch (error) {
|
||||
assert(
|
||||
error instanceof SkillApiContractError,
|
||||
"expected SkillApiContractError",
|
||||
);
|
||||
assert(
|
||||
!error.message.includes(secret),
|
||||
"diagnostic leaked Skill body content",
|
||||
);
|
||||
assert(error.message.length <= 256, "diagnostic must remain bounded");
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user