Merge pull request #34261 from backstage/session/scheming-finch-ks70

This commit is contained in:
Ben Lambert
2026-05-19 16:00:41 +02:00
committed by GitHub
24 changed files with 1283 additions and 2 deletions
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-backend-module-ai-model': minor
'@backstage/catalog-model': minor
---
Introduced the `AiResource` catalog entity kind. Entity types, validators, type guards, and the model layer are exported from `@backstage/catalog-model/alpha`. Install `@backstage/plugin-catalog-backend-module-ai-model` in your backend to register the kind with the catalog.
+1
View File
@@ -280,6 +280,7 @@ catalog:
pullRequestBranchName: backstage-integration
rules:
- allow:
- AiResource
- Component
- API
- Resource
+1
View File
@@ -40,6 +40,7 @@
"@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-backend-module-ai-model": "workspace:^",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^",
"@backstage/plugin-catalog-backend-module-logs": "workspace:^",
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^",
+1
View File
@@ -46,6 +46,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider'));
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed'));
backend.add(import('@backstage/plugin-catalog-backend-module-ai-model'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
@@ -0,0 +1,18 @@
apiVersion: backstage.io/v1alpha1
kind: AiResource
metadata:
name: frontend-design
description: Skill for creating production-grade frontend interfaces using the internal design system
spec:
type: skill
lifecycle: production
owner: team-a
system: artist-engagement-portal
disciplines:
- web
categories:
- framework
- design-system
agents:
- claude-code
- copilot
@@ -0,0 +1,15 @@
apiVersion: backstage.io/v1alpha1
kind: AiResource
metadata:
name: use-internal-apis
description: Agents must use internal API clients instead of raw HTTP calls
spec:
type: rule
lifecycle: production
owner: team-a
system: artist-engagement-portal
disciplines:
- web
- backend
category: architecture
rationale: Ensures consistent error handling, authentication, and observability across all service calls
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: example-ai-resources
description: A collection of all Backstage example AI resources
spec:
targets:
- ./ai-resources/frontend-design-skill.yaml
- ./ai-resources/use-internal-apis-rule.yaml
+1
View File
@@ -5,6 +5,7 @@ metadata:
description: A collection of all Backstage example entities, except users, groups, and templates
spec:
targets:
- ./all-ai-resources.yaml
- ./all-apis.yaml
- ./all-components.yaml
- ./all-domains.yaml
+123 -2
View File
@@ -3,13 +3,40 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import type { Entity } from '@backstage/catalog-model';
import type { Entity as Entity_2 } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { SerializedError } from '@backstage/errors';
// @alpha
export interface AlphaEntity extends Entity {
export const aiResourceEntityModel: CatalogModelLayer;
// @alpha
export type AiResourceEntityV1alpha1 =
| AiResourceEntityV1alpha1Default
| SkillAiResourceEntityV1alpha1
| RuleAiResourceEntityV1alpha1;
// @alpha
export interface AiResourceEntityV1alpha1Default extends Entity {
// (undocumented)
apiVersion: 'backstage.io/v1alpha1';
// (undocumented)
kind: 'AiResource';
// (undocumented)
spec: {
type: string;
lifecycle: string;
owner: string;
system?: string;
};
}
// @alpha
export const aiResourceEntityV1alpha1Validator: KindValidator;
// @alpha
export interface AlphaEntity extends Entity_2 {
status?: EntityStatus;
}
@@ -400,6 +427,43 @@ export function createCatalogModelLayerBuilder(options: {
// @alpha
export const defaultCatalogEntityModel: CatalogModelLayer;
// @public
export type Entity = {
apiVersion: string;
kind: string;
metadata: EntityMeta;
spec?: JsonObject;
relations?: EntityRelation[];
};
// @public
export type EntityLink = {
url: string;
title?: string;
icon?: string;
type?: string;
};
// @public
export type EntityMeta = JsonObject & {
uid?: string;
etag?: string;
name: string;
namespace?: string;
title?: string;
description?: string;
labels?: Record<string, string>;
annotations?: Record<string, string>;
tags?: string[];
links?: EntityLink[];
};
// @public
export type EntityRelation = {
type: string;
targetRef: string;
};
// @alpha
export type EntityStatus = {
items?: EntityStatusItem[];
@@ -416,5 +480,62 @@ export type EntityStatusItem = {
// @alpha
export type EntityStatusLevel = 'info' | 'warning' | 'error';
// @alpha
export const isAiResourceEntity: (
entity: Entity,
) => entity is AiResourceEntityV1alpha1;
// @alpha
export const isRuleAiResourceEntity: (
entity: Entity,
) => entity is RuleAiResourceEntityV1alpha1;
// @alpha
export const isSkillAiResourceEntity: (
entity: Entity,
) => entity is SkillAiResourceEntityV1alpha1;
// @public
export type KindValidator = {
check(entity: Entity): Promise<boolean>;
};
// @alpha
export interface RuleAiResourceEntityV1alpha1
extends AiResourceEntityV1alpha1Default {
// (undocumented)
spec: {
type: 'rule';
lifecycle: string;
owner: string;
system?: string;
disciplines?: string[];
category: string;
rationale: string;
};
}
// @alpha
export const ruleAiResourceEntityV1alpha1Validator: KindValidator;
// @alpha
export interface SkillAiResourceEntityV1alpha1
extends AiResourceEntityV1alpha1Default {
// (undocumented)
spec: {
type: 'skill';
lifecycle: string;
owner: string;
system?: string;
disciplines?: string[];
categories?: string[];
agents?: string[];
dependsOn?: string[];
};
}
// @alpha
export const skillAiResourceEntityV1alpha1Validator: KindValidator;
// (No @packageDocumentation comment for this package)
```
+22
View File
@@ -14,11 +14,33 @@
* limitations under the License.
*/
export type {
Entity,
EntityLink,
EntityMeta,
EntityRelation,
} from './entity/Entity';
export type { KindValidator } from './kinds/types';
export type { AlphaEntity } from './entity/AlphaEntity';
export type {
EntityStatus,
EntityStatusItem,
EntityStatusLevel,
} from './entity/EntityStatus';
export type {
AiResourceEntityV1alpha1,
AiResourceEntityV1alpha1Default,
SkillAiResourceEntityV1alpha1,
RuleAiResourceEntityV1alpha1,
} from './kinds/AiResourceEntityV1alpha1';
export {
aiResourceEntityV1alpha1Validator,
skillAiResourceEntityV1alpha1Validator,
ruleAiResourceEntityV1alpha1Validator,
isAiResourceEntity,
isSkillAiResourceEntity,
isRuleAiResourceEntity,
aiResourceEntityModel,
} from './kinds/AiResourceEntityV1alpha1';
export * from './model';
export { defaultCatalogEntityModel } from './model/defaultCatalogEntityModel';
@@ -0,0 +1,359 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed 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 type { Entity } from '../entity/Entity';
import {
type AiResourceEntityV1alpha1Default,
type SkillAiResourceEntityV1alpha1,
type RuleAiResourceEntityV1alpha1,
aiResourceEntityV1alpha1Validator as defaultValidator,
skillAiResourceEntityV1alpha1Validator as skillValidator,
ruleAiResourceEntityV1alpha1Validator as ruleValidator,
isAiResourceEntity,
isSkillAiResourceEntity,
isRuleAiResourceEntity,
} from './AiResourceEntityV1alpha1';
describe('AiResourceV1alpha1 default validator', () => {
let entity: AiResourceEntityV1alpha1Default;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: {
name: 'internal-design-system',
},
spec: {
type: 'rule',
lifecycle: 'production',
owner: 'frontend-platform',
system: 'ai-tooling',
},
};
});
it('accepts valid data', async () => {
await expect(defaultValidator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(defaultValidator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(defaultValidator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(defaultValidator.check(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(defaultValidator.check(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(defaultValidator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing lifecycle', async () => {
delete (entity as any).spec.lifecycle;
await expect(defaultValidator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects wrong lifecycle', async () => {
(entity as any).spec.lifecycle = 7;
await expect(defaultValidator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects empty lifecycle', async () => {
(entity as any).spec.lifecycle = '';
await expect(defaultValidator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects missing owner', async () => {
delete (entity as any).spec.owner;
await expect(defaultValidator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects wrong owner', async () => {
(entity as any).spec.owner = 7;
await expect(defaultValidator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects empty owner', async () => {
(entity as any).spec.owner = '';
await expect(defaultValidator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(defaultValidator.check(entity)).resolves.toBe(true);
});
it('rejects wrong system', async () => {
(entity as any).spec.system = 7;
await expect(defaultValidator.check(entity)).rejects.toThrow(/system/);
});
it('rejects empty system', async () => {
(entity as any).spec.system = '';
await expect(defaultValidator.check(entity)).rejects.toThrow(/system/);
});
});
describe('AiResourceV1alpha1 skill validator', () => {
let entity: SkillAiResourceEntityV1alpha1;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: {
name: 'frontend-design',
},
spec: {
type: 'skill',
lifecycle: 'production',
owner: 'ai-platform-team',
system: 'ai-tooling',
disciplines: ['web', 'backend'],
categories: ['framework'],
agents: ['claude-code'],
dependsOn: ['airesource:default/base-coding-standards'],
},
};
});
it('accepts valid skill data with all fields', async () => {
await expect(skillValidator.check(entity)).resolves.toBe(true);
});
it('accepts skill with only required fields', async () => {
entity.spec = {
type: 'skill',
lifecycle: 'experimental',
owner: 'team-a',
};
await expect(skillValidator.check(entity)).resolves.toBe(true);
});
it('rejects non-skill type', async () => {
(entity as any).spec.type = 'rule';
await expect(skillValidator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing lifecycle', async () => {
delete (entity as any).spec.lifecycle;
await expect(skillValidator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects missing owner', async () => {
delete (entity as any).spec.owner;
await expect(skillValidator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing optional fields', async () => {
delete (entity as any).spec.system;
delete (entity as any).spec.disciplines;
delete (entity as any).spec.categories;
delete (entity as any).spec.agents;
delete (entity as any).spec.dependsOn;
await expect(skillValidator.check(entity)).resolves.toBe(true);
});
it('rejects disciplines with empty strings', async () => {
(entity as any).spec.disciplines = [''];
await expect(skillValidator.check(entity)).rejects.toThrow(/disciplines/);
});
it('rejects categories with wrong type', async () => {
(entity as any).spec.categories = 'not-an-array';
await expect(skillValidator.check(entity)).rejects.toThrow(/categories/);
});
it('rejects agents with wrong item type', async () => {
(entity as any).spec.agents = [42];
await expect(skillValidator.check(entity)).rejects.toThrow(/agents/);
});
it('rejects dependsOn with empty strings', async () => {
(entity as any).spec.dependsOn = [''];
await expect(skillValidator.check(entity)).rejects.toThrow(/dependsOn/);
});
});
describe('AiResourceV1alpha1 rule validator', () => {
let entity: RuleAiResourceEntityV1alpha1;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: {
name: 'use-internal-apis',
},
spec: {
type: 'rule',
lifecycle: 'production',
owner: 'frontend-platform',
disciplines: ['web', 'backend'],
category: 'architecture',
rationale: 'Ensures consistent error handling across all service calls',
},
};
});
it('accepts valid rule data with all fields', async () => {
await expect(ruleValidator.check(entity)).resolves.toBe(true);
});
it('accepts rule with only required fields', async () => {
entity.spec = {
type: 'rule',
lifecycle: 'production',
owner: 'team-a',
category: 'security',
rationale: 'Prevents credential leaks',
};
await expect(ruleValidator.check(entity)).resolves.toBe(true);
});
it('rejects non-rule type', async () => {
(entity as any).spec.type = 'skill';
await expect(ruleValidator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing category', async () => {
delete (entity as any).spec.category;
await expect(ruleValidator.check(entity)).rejects.toThrow(/category/);
});
it('rejects empty category', async () => {
(entity as any).spec.category = '';
await expect(ruleValidator.check(entity)).rejects.toThrow(/category/);
});
it('rejects missing rationale', async () => {
delete (entity as any).spec.rationale;
await expect(ruleValidator.check(entity)).rejects.toThrow(/rationale/);
});
it('rejects empty rationale', async () => {
(entity as any).spec.rationale = '';
await expect(ruleValidator.check(entity)).rejects.toThrow(/rationale/);
});
it('accepts missing optional fields', async () => {
delete (entity as any).spec.system;
delete (entity as any).spec.disciplines;
await expect(ruleValidator.check(entity)).resolves.toBe(true);
});
it('rejects disciplines with empty strings', async () => {
(entity as any).spec.disciplines = [''];
await expect(ruleValidator.check(entity)).rejects.toThrow(/disciplines/);
});
});
describe('isAiResourceEntity', () => {
it('returns true when apiVersion and kind match', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: { name: 'test' },
};
expect(isAiResourceEntity(entity)).toBe(true);
});
it('returns false for a different kind', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'test' },
};
expect(isAiResourceEntity(entity)).toBe(false);
});
it('returns false for a different apiVersion', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'AiResource',
metadata: { name: 'test' },
};
expect(isAiResourceEntity(entity)).toBe(false);
});
});
describe('isSkillAiResourceEntity', () => {
it('returns true for a skill AiResource', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: { name: 'test' },
spec: { type: 'skill' },
};
expect(isSkillAiResourceEntity(entity)).toBe(true);
});
it('returns false for a non-skill AiResource', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: { name: 'test' },
spec: { type: 'rule' },
};
expect(isSkillAiResourceEntity(entity)).toBe(false);
});
it('returns false for a different kind', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'test' },
spec: { type: 'skill' },
};
expect(isSkillAiResourceEntity(entity)).toBe(false);
});
});
describe('isRuleAiResourceEntity', () => {
it('returns true for a rule AiResource', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: { name: 'test' },
spec: { type: 'rule' },
};
expect(isRuleAiResourceEntity(entity)).toBe(true);
});
it('returns false for a non-rule AiResource', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'AiResource',
metadata: { name: 'test' },
spec: { type: 'skill' },
};
expect(isRuleAiResourceEntity(entity)).toBe(false);
});
});
@@ -0,0 +1,236 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed 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 { createCatalogModelLayer } from '../model/createCatalogModelLayer';
import type { Entity } from '../entity/Entity';
import { entityKindSchemaValidator } from '../validation';
import type { KindValidator } from './types';
import type { JsonObject } from '@backstage/types';
import defaultJsonSchema from '../schema/kinds/AiResource.v1alpha1.schema.json';
import skillJsonSchema from '../schema/kinds/AiResource.v1alpha1.skill.schema.json';
import ruleJsonSchema from '../schema/kinds/AiResource.v1alpha1.rule.schema.json';
/**
* Default AiResource entity for types that don't have a structured spec.
*
* @remarks
*
* The actual content of skills and rules is not stored in the entity spec.
* Instead, the source file is referenced via the standard
* `backstage.io/source-location` annotation, consistent with how other
* Backstage entities reference their source files.
*
* @alpha
*/
export interface AiResourceEntityV1alpha1Default extends Entity {
apiVersion: 'backstage.io/v1alpha1';
kind: 'AiResource';
spec: {
type: string;
lifecycle: string;
owner: string;
system?: string;
};
}
/**
* AiResource entity with spec.type 'skill'. Represents reusable contextual
* knowledge consumed by AI coding tools.
*
* @alpha
*/
export interface SkillAiResourceEntityV1alpha1
extends AiResourceEntityV1alpha1Default {
spec: {
type: 'skill';
lifecycle: string;
owner: string;
system?: string;
disciplines?: string[];
categories?: string[];
agents?: string[];
dependsOn?: string[];
};
}
/**
* AiResource entity with spec.type 'rule'. Represents a governance rule
* or constraint for AI coding tools.
*
* @alpha
*/
export interface RuleAiResourceEntityV1alpha1
extends AiResourceEntityV1alpha1Default {
spec: {
type: 'rule';
lifecycle: string;
owner: string;
system?: string;
disciplines?: string[];
category: string;
rationale: string;
};
}
/**
* Backstage catalog AiResource kind Entity. Represents contextual information
* consumed by AI coding tools, such as skills and rules.
*
* @alpha
*/
export type AiResourceEntityV1alpha1 =
| AiResourceEntityV1alpha1Default
| SkillAiResourceEntityV1alpha1
| RuleAiResourceEntityV1alpha1;
const defaultValidator = entityKindSchemaValidator(defaultJsonSchema);
/**
* Entity data validator for the default {@link AiResourceEntityV1alpha1}.
*
* @alpha
*/
export const aiResourceEntityV1alpha1Validator: KindValidator = {
async check(data: Entity) {
return defaultValidator(data) === data;
},
};
const skillValidator = entityKindSchemaValidator(skillJsonSchema);
/**
* Entity data validator for {@link SkillAiResourceEntityV1alpha1}.
*
* @alpha
*/
export const skillAiResourceEntityV1alpha1Validator: KindValidator = {
async check(data: Entity) {
return skillValidator(data) === data;
},
};
/**
* Type guard for {@link AiResourceEntityV1alpha1}.
*
* @alpha
*/
export const isAiResourceEntity = (
entity: Entity,
): entity is AiResourceEntityV1alpha1 =>
entity.apiVersion === 'backstage.io/v1alpha1' && entity.kind === 'AiResource';
/**
* Type guard for {@link SkillAiResourceEntityV1alpha1}.
*
* @alpha
*/
export const isSkillAiResourceEntity = (
entity: Entity,
): entity is SkillAiResourceEntityV1alpha1 =>
isAiResourceEntity(entity) && entity.spec?.type === 'skill';
/**
* Type guard for {@link RuleAiResourceEntityV1alpha1}.
*
* @alpha
*/
export const isRuleAiResourceEntity = (
entity: Entity,
): entity is RuleAiResourceEntityV1alpha1 =>
isAiResourceEntity(entity) && entity.spec?.type === 'rule';
const ruleValidator = entityKindSchemaValidator(ruleJsonSchema);
/**
* Entity data validator for {@link RuleAiResourceEntityV1alpha1}.
*
* @alpha
*/
export const ruleAiResourceEntityV1alpha1Validator: KindValidator = {
async check(data: Entity) {
return ruleValidator(data) === data;
},
};
const baseRelationFields = [
{
selector: { path: 'spec.owner' },
relation: 'ownedBy',
defaultKind: 'Group',
defaultNamespace: 'inherit' as const,
allowedKinds: ['Group', 'User'],
},
{
selector: { path: 'spec.system' },
relation: 'partOf',
defaultKind: 'System',
defaultNamespace: 'inherit' as const,
},
];
/**
* Extends the catalog model with the AiResource kind.
*
* @alpha
*/
export const aiResourceEntityModel = createCatalogModelLayer({
layerId: 'catalog.backstage.io/kind-ai-resource',
builder: model => {
model.addKind({
group: 'backstage.io',
names: {
kind: 'AiResource',
singular: 'airesource',
plural: 'airesources',
},
description:
'An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.',
versions: [
{
name: 'v1alpha1',
relationFields: baseRelationFields,
schema: {
jsonSchema: defaultJsonSchema as JsonObject,
},
},
{
name: 'v1alpha1',
specType: 'skill',
relationFields: [
...baseRelationFields,
{
selector: { path: 'spec.dependsOn' },
relation: 'dependsOn',
defaultKind: 'AiResource',
defaultNamespace: 'inherit' as const,
},
],
schema: {
jsonSchema: skillJsonSchema as JsonObject,
},
},
{
name: 'v1alpha1',
specType: 'rule',
relationFields: baseRelationFields,
schema: {
jsonSchema: ruleJsonSchema as JsonObject,
},
},
],
});
},
});
@@ -0,0 +1,87 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AiResourceV1alpha1Rule",
"description": "An AI resource of type 'rule', representing a governance rule or constraint for AI coding tools.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "AiResource",
"metadata": {
"name": "use-internal-apis",
"description": "Agents must use internal API clients instead of raw HTTP calls"
},
"spec": {
"type": "rule",
"lifecycle": "production",
"owner": "frontend-platform",
"disciplines": ["web", "backend"],
"category": "architecture",
"rationale": "Ensures consistent error handling, authentication, and observability across all service calls"
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1"]
},
"kind": {
"enum": ["AiResource"]
},
"spec": {
"type": "object",
"required": ["type", "lifecycle", "owner", "category", "rationale"],
"properties": {
"type": {
"type": "string",
"enum": ["rule"],
"description": "The type of AI resource."
},
"lifecycle": {
"type": "string",
"description": "The lifecycle state of the AI resource.",
"examples": ["experimental", "production", "deprecated"],
"minLength": 1
},
"owner": {
"type": "string",
"description": "An entity reference to the owner of the AI resource.",
"examples": ["frontend-platform", "user:john.johnson"],
"minLength": 1
},
"system": {
"type": "string",
"description": "An entity reference to the system that the AI resource belongs to.",
"minLength": 1
},
"disciplines": {
"type": "array",
"description": "Engineering disciplines this rule applies to.",
"items": {
"type": "string",
"minLength": 1
}
},
"category": {
"type": "string",
"description": "The category of this rule.",
"examples": ["security", "architecture", "style"],
"minLength": 1
},
"rationale": {
"type": "string",
"description": "Explanation of why this rule exists.",
"minLength": 1
}
}
}
}
}
]
}
@@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AiResourceV1alpha1",
"description": "An AI resource represents contextual information consumed by AI coding tools, such as skills and rules.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "AiResource",
"metadata": {
"name": "frontend-design",
"description": "Skill for creating production-grade frontend interfaces"
},
"spec": {
"type": "skill",
"lifecycle": "production",
"owner": "ai-platform-team",
"system": "ai-tooling"
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1"]
},
"kind": {
"enum": ["AiResource"]
},
"spec": {
"type": "object",
"required": ["type", "lifecycle", "owner"],
"properties": {
"type": {
"type": "string",
"description": "The type of AI resource.",
"examples": ["skill", "rule"],
"minLength": 1
},
"lifecycle": {
"type": "string",
"description": "The lifecycle state of the AI resource.",
"examples": ["experimental", "production", "deprecated"],
"minLength": 1
},
"owner": {
"type": "string",
"description": "An entity reference to the owner of the AI resource.",
"examples": ["ai-platform-team", "user:john.johnson"],
"minLength": 1
},
"system": {
"type": "string",
"description": "An entity reference to the system that the AI resource belongs to.",
"minLength": 1
}
}
}
}
}
]
}
@@ -0,0 +1,101 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AiResourceV1alpha1Skill",
"description": "An AI resource of type 'skill', representing reusable contextual knowledge consumed by AI coding tools.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "AiResource",
"metadata": {
"name": "frontend-design",
"description": "Skill for creating production-grade frontend interfaces"
},
"spec": {
"type": "skill",
"lifecycle": "production",
"owner": "ai-platform-team",
"system": "ai-tooling",
"disciplines": ["web"],
"categories": ["framework"],
"agents": ["claude-code"]
}
}
],
"allOf": [
{
"$ref": "Entity"
},
{
"type": "object",
"required": ["spec"],
"properties": {
"apiVersion": {
"enum": ["backstage.io/v1alpha1"]
},
"kind": {
"enum": ["AiResource"]
},
"spec": {
"type": "object",
"required": ["type", "lifecycle", "owner"],
"properties": {
"type": {
"type": "string",
"enum": ["skill"],
"description": "The type of AI resource."
},
"lifecycle": {
"type": "string",
"description": "The lifecycle state of the AI resource.",
"examples": ["experimental", "production", "deprecated"],
"minLength": 1
},
"owner": {
"type": "string",
"description": "An entity reference to the owner of the AI resource.",
"examples": ["ai-platform-team", "user:john.johnson"],
"minLength": 1
},
"system": {
"type": "string",
"description": "An entity reference to the system that the AI resource belongs to.",
"minLength": 1
},
"disciplines": {
"type": "array",
"description": "Engineering disciplines this skill applies to.",
"items": {
"type": "string",
"minLength": 1
}
},
"categories": {
"type": "array",
"description": "Descriptive categories for the skill.",
"items": {
"type": "string",
"minLength": 1
}
},
"agents": {
"type": "array",
"description": "AI coding tools this skill is designed for.",
"items": {
"type": "string",
"minLength": 1
}
},
"dependsOn": {
"type": "array",
"description": "Entity references to other AI resources this skill depends on.",
"items": {
"type": "string",
"minLength": 1
}
}
}
}
}
}
]
}
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,45 @@
# @backstage/plugin-catalog-backend-module-ai-model
Adds support for the `AiResource` entity kind to the catalog backend plugin. AI resources represent contextual information consumed by AI coding tools, such as skills and rules.
## Installation
Add the module to your backend:
```ts
backend.add(import('@backstage/plugin-catalog-backend-module-ai-model'));
```
## Entity shape
```yaml
apiVersion: backstage.io/v1alpha1
kind: AiResource
metadata:
name: frontend-design
description: Skill for creating production-grade frontend interfaces
spec:
type: skill
lifecycle: production
owner: ai-platform-team
system: ai-tooling
disciplines:
- web
categories:
- framework
agents:
- claude-code
dependsOn:
- airesource:default/base-coding-standards
```
The `type` field determines which spec fields are available. Currently supported types:
- **`skill`** — reusable contextual knowledge for AI coding tools. Supports additional fields: `disciplines`, `categories`, `agents`, `dependsOn`.
- **`rule`** — governance rules and constraints for AI coding tools. Supports additional fields: `disciplines`, `category` (required), `rationale` (required).
Any other `type` value is accepted with the base spec fields: `type`, `lifecycle`, `owner`, and optionally `system`.
## Accessing skill and rule content
The actual content of skills and rules is not stored in the entity spec. Instead, the source file is referenced via the standard `backstage.io/source-location` annotation, consistent with how other Backstage entities reference their source files. Entity providers that generate `AiResource` entities from `SKILL.md` or rule files should set this annotation to point to the source file.
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-catalog-backend-module-ai-model
title: '@backstage/plugin-catalog-backend-module-ai-model'
description: Adds support for the AIResource entity kind to the catalog backend plugin.
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
@@ -0,0 +1,53 @@
{
"name": "@backstage/plugin-catalog-backend-module-ai-model",
"version": "0.0.0",
"description": "Adds support for the AiResource entity kind to the catalog backend plugin.",
"backstage": {
"role": "backend-plugin-module",
"pluginId": "catalog",
"pluginPackage": "@backstage/plugin-catalog-backend"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/catalog-backend-module-ai-model"
},
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"package.json": [
"package.json"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^"
}
}
@@ -0,0 +1,11 @@
## API Report File for "@backstage/plugin-catalog-backend-module-ai-model"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
// @public
const catalogModuleAiResourceEntityModel: BackendFeature;
export default catalogModuleAiResourceEntityModel;
```
@@ -0,0 +1,23 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed 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.
*/
/**
* Adds support for the AiResource entity kind to the catalog backend plugin.
*
* @packageDocumentation
*/
export { catalogModuleAiResourceEntityModel as default } from './module';
@@ -0,0 +1,36 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed 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 { startTestBackend } from '@backstage/backend-test-utils';
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { catalogModuleAiResourceEntityModel } from './module';
describe('catalogModuleAiResourceEntityModel', () => {
it('should register the model source', async () => {
const extensionPoint = {
setFieldValidators: jest.fn(),
setEntityDataParser: jest.fn(),
addModelSource: jest.fn(),
};
await startTestBackend({
extensionPoints: [[catalogModelExtensionPoint, extensionPoint]],
features: [catalogModuleAiResourceEntityModel],
});
expect(extensionPoint.addModelSource).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,44 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed 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 { createBackendModule } from '@backstage/backend-plugin-api';
import {
CatalogModelSources,
aiResourceEntityModel,
} from '@backstage/catalog-model/alpha';
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
/**
* Registers support for the AiResource entity kind in the catalog.
*
* @public
*/
export const catalogModuleAiResourceEntityModel = createBackendModule({
pluginId: 'catalog',
moduleId: 'ai-model',
register(reg) {
reg.registerInit({
deps: {
model: catalogModelExtensionPoint,
},
async init({ model }) {
model.addModelSource(
CatalogModelSources.static([aiResourceEntityModel]),
);
},
});
},
});
+13
View File
@@ -4759,6 +4759,18 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-ai-model@workspace:^, @backstage/plugin-catalog-backend-module-ai-model@workspace:plugins/catalog-backend-module-ai-model":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-ai-model@workspace:plugins/catalog-backend-module-ai-model"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-aws@workspace:plugins/catalog-backend-module-aws"
@@ -29292,6 +29304,7 @@ __metadata:
"@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-catalog-backend-module-ai-model": "workspace:^"
"@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^"
"@backstage/plugin-catalog-backend-module-logs": "workspace:^"
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^"