| 1 | /** Reviewed bundle roots contribute to Rust's existing skill catalog. */ |
| 2 | import { Service } from '@deepseek-ai/cordis' |
| 3 | import type { RpcPeer } from '../rpc.ts' |
| 4 | import { OwnedRegistrations, type OwnedEntry, type OwnerBase } from './owned.ts' |
| 5 | |
| 6 | export const MAX_SKILL_ROOTS_PER_OWNER = 8 |
| 7 | export const MAX_SKILL_ROOTS_PER_HOST = 64 |
| 8 | export interface SkillRootDefinition { readonly path: string } |
| 9 | export interface LocalSkillRoot<O extends OwnerBase = OwnerBase> extends OwnedEntry<O> {} |
| 10 | |
| 11 | export function normalizeSkillRoot(value: unknown): SkillRootDefinition { |
| 12 | if (typeof value !== 'object' || value === null || Array.isArray(value) |
| 13 | || Object.keys(value).some(key => key !== 'path')) throw new TypeError('skill root supports only path') |
| 14 | const { path } = value as { path?: unknown } |
| 15 | if (typeof path !== 'string' || !path || Buffer.byteLength(path, 'utf8') > 512 |
| 16 | || /[\\:\u0000-\u001f\u007f-\u009f]/u.test(path) |
| 17 | || path.split('/').some(part => !part || part === '.' || part === '..')) { |
| 18 | throw new TypeError('skill root path must be a bounded bundle-relative path with normal slash-separated components') |
| 19 | } |
| 20 | return Object.freeze({ path }) |
| 21 | } |
| 22 | |
| 23 | export class SkillRoots<O extends OwnerBase> { |
| 24 | private readonly registrations: OwnedRegistrations<O, LocalSkillRoot<O>> |
| 25 | private readonly owners = new Map<O, Map<string, () => void>>() |
| 26 | private count = 0 |
| 27 | constructor(rpc: RpcPeer, ownedBy: (owner: O) => Map<number, LocalSkillRoot<O>>, warn: (message: string, owner: O) => void) { |
| 28 | this.registrations = new OwnedRegistrations(rpc, 'skill_root', ownedBy, warn) |
| 29 | } |
| 30 | register(owner: O, definition: SkillRootDefinition): () => void { |
| 31 | if (owner.state !== 'activating' && owner.state !== 'active') throw new Error('skill owner is not live') |
| 32 | const { path } = normalizeSkillRoot(definition) |
| 33 | const roots = this.owners.get(owner) ?? new Map<string, () => void>() |
| 34 | if (roots.has(path)) throw new Error('skill root is already registered; dispose it before registering it again') |
| 35 | if (roots.size >= MAX_SKILL_ROOTS_PER_OWNER || this.count >= MAX_SKILL_ROOTS_PER_HOST) throw new RangeError('skill root owner or host registration limit reached') |
| 36 | const undo = this.registrations.add({ owner, name: path, disposed: false }, { name: path, description: '' }) |
| 37 | const dispose = () => { |
| 38 | if (roots.get(path) !== dispose) return |
| 39 | roots.delete(path) |
| 40 | this.count-- |
| 41 | if (!roots.size) this.owners.delete(owner) |
| 42 | undo() |
| 43 | } |
| 44 | roots.set(path, dispose) |
| 45 | this.owners.set(owner, roots) |
| 46 | this.count++ |
| 47 | return dispose |
| 48 | } |
| 49 | forget(owner: O) { |
| 50 | for (const dispose of [...(this.owners.get(owner)?.values() ?? [])]) dispose() |
| 51 | this.registrations.forget(owner) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | export function defineSkillsService<O extends OwnerBase>(host: { ownerOf(ctx: any): O | undefined, skillRoots: SkillRoots<O> }) { |
| 56 | class SkillsShim extends Service { |
| 57 | constructor(ctx: any) { super(ctx, 'skills') } |
| 58 | registerRoot(definition: SkillRootDefinition): () => void { |
| 59 | const ctx: any = this.ctx |
| 60 | const owner = host.ownerOf(ctx) |
| 61 | if (!owner) throw new Error('skills.registerRoot called outside an extension owner') |
| 62 | const root = normalizeSkillRoot(definition) |
| 63 | return ctx.effect(() => host.skillRoots.register(owner, root), `skills.registerRoot(${JSON.stringify(root.path)})`) |
| 64 | } |
| 65 | } |
| 66 | Object.freeze(SkillsShim.prototype) |
| 67 | return SkillsShim |
| 68 | } |
| 69 |