Browse Source

feat(context): implement runtime discovery and permission validation (#238)

Protocol Zero 1 month ago
parent
commit
25db9470cc

+ 72 - 0
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<ContextDefinition[]> {
+    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<LoadedContext | null> {
+    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;
+  }
+}

+ 35 - 0
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<typeof ContextDefinitionSchema>;
+
+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<typeof SkillPermissionSchema>;
+export type AgentPermissions = z.infer<typeof AgentPermissionsSchema>;

+ 5 - 0
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'

+ 45 - 0
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;
+  }
+}