diff --git a/packages/plugin-abilities/src/context/discovery.ts b/packages/plugin-abilities/src/context/discovery.ts new file mode 100644 index 0000000..045ba1a --- /dev/null +++ b/packages/plugin-abilities/src/context/discovery.ts @@ -0,0 +1,72 @@ +import { glob } from 'glob'; +import path from 'path'; +import fs from 'fs/promises'; +import { parse as parseYaml } from 'yaml'; +import { ContextDefinitionSchema, type ContextDefinition, type LoadedContext } from './types.js'; + +export interface DiscoveryOptions { + rootDir?: string; + contextDir?: string; +} + +export class ContextDiscovery { + private rootDir: string; + private contextDir: string; + + constructor(options: DiscoveryOptions = {}) { + this.rootDir = options.rootDir || process.cwd(); + this.contextDir = options.contextDir || path.join(this.rootDir, '.opencode', 'context'); + } + + async discover(): Promise { + const files = await glob('**/*.{yaml,yml,json}', { + cwd: this.contextDir, + ignore: ['node_modules/**'], + }); + + const definitions: ContextDefinition[] = []; + + for (const file of files) { + const filePath = path.join(this.contextDir, file); + try { + const content = await fs.readFile(filePath, 'utf-8'); + const parsed = file.endsWith('.json') ? JSON.parse(content) : parseYaml(content); + + // Handle array of definitions or single definition + const items = Array.isArray(parsed) ? parsed : [parsed]; + + for (const item of items) { + const result = ContextDefinitionSchema.safeParse(item); + if (result.success) { + definitions.push(result.data); + } else { + console.warn(`Invalid context definition in ${file}:`, result.error.format()); + } + } + } catch (error) { + console.warn(`Failed to load context file ${file}:`, error); + } + } + + return definitions; + } + + async loadContext(definition: ContextDefinition): Promise { + if (definition.type === 'file') { + const filePath = path.resolve(this.rootDir, definition.path); + try { + const content = await fs.readFile(filePath, 'utf-8'); + return { + definition, + content, + source: filePath, + }; + } catch (error) { + console.warn(`Failed to read context file ${definition.path}:`, error); + return null; + } + } + // TODO: Handle URL and API types + return null; + } +} diff --git a/packages/plugin-abilities/src/context/types.ts b/packages/plugin-abilities/src/context/types.ts new file mode 100644 index 0000000..57f8c22 --- /dev/null +++ b/packages/plugin-abilities/src/context/types.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +export const ContextTypeSchema = z.enum(['file', 'url', 'api', 'snippet']); + +export const ContextDefinitionSchema = z.object({ + id: z.string(), + type: ContextTypeSchema, + path: z.string(), + description: z.string().optional(), + priority: z.number().optional(), + metadata: z.record(z.unknown()).optional(), +}); + +export type ContextDefinition = z.infer; + +export interface LoadedContext { + definition: ContextDefinition; + content: string; + source: string; +} + +export const SkillPermissionSchema = z.object({ + skill: z.string(), + tools: z.array(z.string()).optional(), + resources: z.array(z.string()).optional(), + description: z.string().optional(), +}); + +export const AgentPermissionsSchema = z.object({ + agent: z.string(), + permissions: z.array(SkillPermissionSchema), +}); + +export type SkillPermission = z.infer; +export type AgentPermissions = z.infer; diff --git a/packages/plugin-abilities/src/index.ts b/packages/plugin-abilities/src/index.ts index f5e8fb2..79110d8 100644 --- a/packages/plugin-abilities/src/index.ts +++ b/packages/plugin-abilities/src/index.ts @@ -24,6 +24,11 @@ export { loadAbilities, loadAbility } from './loader/index.js' // Validator export { validateAbility, validateInputs } from './validator/index.js' +export { PermissionValidator } from './validator/permissions.js' + +// Context Discovery +export { ContextDiscovery } from './context/discovery.js' +export type { ContextDefinition, LoadedContext, AgentPermissions } from './context/types.js' // Executor export { executeAbility, formatExecutionResult } from './executor/index.js' diff --git a/packages/plugin-abilities/src/validator/permissions.ts b/packages/plugin-abilities/src/validator/permissions.ts new file mode 100644 index 0000000..2a9dd38 --- /dev/null +++ b/packages/plugin-abilities/src/validator/permissions.ts @@ -0,0 +1,45 @@ +import { AgentPermissionsSchema, type AgentPermissions, type SkillPermission } from '../context/types.js'; + +export interface PermissionValidationResult { + valid: boolean; + errors: string[]; +} + +export class PermissionValidator { + validateAgentPermissions(data: unknown): PermissionValidationResult { + const result = AgentPermissionsSchema.safeParse(data); + + if (!result.success) { + return { + valid: false, + errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`), + }; + } + + return { + valid: true, + errors: [], + }; + } + + checkSkillAccess(agentPermissions: AgentPermissions, skillName: string, toolName: string): boolean { + const permission = agentPermissions.permissions.find(p => p.skill === skillName); + + if (!permission) { + // Default deny if no explicit permission for skill + return false; + } + + if (!permission.tools) { + // If tools not specified, assume strict/deny or allow all? + // Security best practice: Default deny. + return false; + } + + if (permission.tools.includes('*') || permission.tools.includes(toolName)) { + return true; + } + + return false; + } +}